31 Commits

Author SHA1 Message Date
Gregory Ballantine
b19c95187b [Issue #16] - Abstracted benchmarks from tests via BenchmarkProfile model, to allow for linking benchmarks with different runtime settings
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-09-11 16:09:00 -04:00
a736113abd Set links to use a transition for color changes
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-08-28 15:22:18 -04:00
9c9b038ef8 Changing woodpecker config to create the DB right before tests
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-08-20 23:17:38 -04:00
85749a4616 Changing woodpecker config to create the DB right before tests
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-08-20 22:57:04 -04:00
619c122769 Adding more unit tests
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-08-20 22:54:23 -04:00
8c5f510c70 Added some more unit tests
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-08-20 10:34:41 -04:00
8b2c152803 Added some Content-Type unit tests
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-08-19 19:15:37 -04:00
98717db3d5 Added project logo to the README
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-08-14 09:35:43 -04:00
5d249eb3c7 Added AI-generated logo/favicon; adjusted some navbar styles
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-08-14 09:28:59 -04:00
4ed915a2c0 Added some basic table sorting
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-08-13 23:20:42 -04:00
39f95575da Version bump to v0.2.1
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
ci/woodpecker/release/woodpecker Pipeline was successful
2025-08-13 14:57:45 -04:00
6e1ab89209 Updating version number in appinfo
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-08-13 14:57:19 -04:00
9cd6c78741 Adding missing step from Docker dev
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-08-13 14:54:02 -04:00
5b730df803 Adding build badge to README
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-08-13 14:52:34 -04:00
0ce4a3ecee Updated README
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-08-13 14:46:38 -04:00
12ece12394 Fixed some lints
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-08-13 14:13:26 -04:00
05c20b5811 Adding a model association test
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-08-13 11:06:55 -04:00
bd822664b0 Added some model tests
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-08-13 09:57:04 -04:00
3f0efce0d8 Fixed a few lints; changed rake task name to test:lint
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-08-12 23:56:46 -04:00
eeedc57cd3 Overhauled configuration so that it's a bit more useful in more spots; configuration now properly loads an environment config as well as defaults; updated some woodpecker config
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-08-12 23:47:38 -04:00
164eea1bde Updated Woodpecker config for 3.x
All checks were successful
ci/woodpecker/manual/woodpecker Pipeline was successful
2025-08-12 21:59:59 -04:00
85fe3b0b38 Removing broken ruby test versions for now
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-08-12 21:55:03 -04:00
519955e57a Fixing unit tests
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-08-12 21:30:17 -04:00
96b746822c Fixing unit tests
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-08-12 21:27:17 -04:00
eac833fd6d Adding db:migrate step to tests; adding two new versions of Ruby to test against
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-08-12 20:29:57 -04:00
ad391c84a4 Fixing ruby 3.4 test
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-08-12 20:23:50 -04:00
641c9315bc Adding Ruby 3.4 testing to Woodpecker
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-08-12 20:03:20 -04:00
3a136865b0 Added some more tests; changed URLs for model list pages and added redirects
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2025-08-12 18:22:22 -04:00
Gregory Ballantine
f40d69a98d Using before hook for index route
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-08-12 16:26:48 -04:00
Gregory Ballantine
40cfdcc2a3 Changed naming from Routes to Controllers; fixed some Sinatra modular layout stuff; added RSpec for testing and some basic tests
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-08-12 16:15:43 -04:00
Gregory Ballantine
260d0d1268 Added rspec testing. It should work, but doesn't for unknown reasons 2025-08-12 15:35:46 -04:00
48 changed files with 1013 additions and 116 deletions

6
.gitignore vendored
View File

@@ -63,5 +63,7 @@ data/
node_modules/
# Compiled assets
public/css/
public/js/
public/*/
# Ignore production configuration files to protect against credential leaks
config/production.yaml

View File

@@ -1,4 +1,6 @@
require: rubocop-sequel
plugins:
- rubocop-rspec
- rubocop-sequel
AllCops:
NewCops: enable

View File

@@ -1,11 +1,32 @@
pipeline:
steps:
setup:
image: ruby:3.4
env:
RACK_ENV: testing
commands:
- gem install rake
- bundle config set --local path "vendor/bundle"
- bundle install
- RACK_ENV=testing rake db:migrate
test_ruby34:
image: ruby:3.4
env:
RACK_ENV: testing
commands:
- gem install rake
- bundle config set --local path "vendor/bundle"
- rake test:unit
group: tests
style:
image: ruby:3.4
env:
RACK_ENV: testing
commands:
- 'gem install rake'
- 'bundle config set --local path "vendor/bundle"'
- 'bundle install'
- 'rake test:rubocop'
- gem install rake
- bundle config set --local path "vendor/bundle"
- rake test:lint
gitea_release:
image: plugins/gitea-release
@@ -15,5 +36,5 @@ pipeline:
base_url: https://git.metaunix.net
title: "${CI_COMMIT_TAG}"
when:
event: tag
event:
- tag

View File

@@ -15,5 +15,12 @@ group :development, :test do
# rubocop and extensions for code style
gem 'rubocop'
gem 'rubocop-rspec'
gem 'rubocop-sequel'
end
group :test do
gem 'rspec'
gem 'rack-test'
gem 'database_cleaner-sequel'
end

View File

@@ -4,6 +4,11 @@ GEM
ast (2.4.3)
base64 (0.3.0)
bigdecimal (3.2.2)
database_cleaner-core (2.0.1)
database_cleaner-sequel (2.0.2)
database_cleaner-core (~> 2.0.0)
sequel
diff-lcs (1.6.2)
ffi (1.17.2-aarch64-linux-gnu)
ffi (1.17.2-aarch64-linux-musl)
ffi (1.17.2-arm-linux-gnu)
@@ -41,6 +46,8 @@ GEM
rack-session (2.1.1)
base64 (>= 0.1.0)
rack (>= 3.0.0)
rack-test (2.2.0)
rack (>= 1.3)
rainbow (3.1.1)
rb-fsevent (0.11.2)
rb-inotify (0.11.1)
@@ -48,6 +55,19 @@ GEM
regexp_parser (2.10.0)
rerun (0.14.0)
listen (~> 3.0)
rspec (3.13.1)
rspec-core (~> 3.13.0)
rspec-expectations (~> 3.13.0)
rspec-mocks (~> 3.13.0)
rspec-core (3.13.5)
rspec-support (~> 3.13.0)
rspec-expectations (3.13.5)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-mocks (3.13.5)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-support (3.13.4)
rubocop (1.76.1)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
@@ -62,6 +82,9 @@ GEM
rubocop-ast (1.45.1)
parser (>= 3.3.7.2)
prism (~> 1.4)
rubocop-rspec (3.6.0)
lint_roller (~> 1.1)
rubocop (~> 1.72, >= 1.72.1)
rubocop-sequel (0.4.1)
lint_roller (~> 1.1)
rubocop (>= 1.72.1, < 2)
@@ -110,10 +133,14 @@ PLATFORMS
x86_64-linux-musl
DEPENDENCIES
database_cleaner-sequel
logger
puma (~> 6.6)
rack-test
rerun
rspec
rubocop
rubocop-rspec
rubocop-sequel
sequel (~> 5.92)
sinatra (~> 4.1)

View File

@@ -1,3 +1,7 @@
![Game Data Logo](https://git.metaunix.net/BitGoblin/game-data/raw/branch/main/assets/img/app-logo.png)
![Build badge](https://builds.metaunix.net/api/badges/84/status.svg)
# Game Data
Web-based tool to store and organize PC hardware gaming benchmarks.
@@ -7,28 +11,58 @@ Web-based tool to store and organize PC hardware gaming benchmarks.
The goals of this project are to:
* Record benchmarking results from multiple devices - e.g. log from a laptop or a phone.
* Group results into tests - it's good practice to run a benchmark multiple times for accuracy.
* Group results into tests to keep track of different testing configurations.
* Encourage running tests multiple times - it's good practice to run a benchmark multiple times for accuracy.
* Create comparisons of hardware tests to compare performance.
* Generate graphs of hardware comparisons for usage in videos and articles.
## Requirements
Game Data runs on Ruby, and takes advantage of Bundler to manage code dependencies and Rake to run various tasks for maintaining the app. You can install them globally like so:
Game Data runs on Ruby, and takes advantage of [Bundler](https://bundler.io/) to manage code dependencies and [Rake](https://ruby.github.io/rake/) to run various tasks for maintaining the app. You can install them globally like so:
Debian/Ubuntu: `apt install -y ruby ruby-bundler rake`
RedHat and clones: `dnf install -y ruby rubygem-bundler rubygem-rake`
## Production Deployment
**TBD**
## Development
Install dependencies via bundler:
### Via Docker
If you'd prefer not to install dependencies and such to your local OS, you can do the development via Docker. The scripts provided in `bin/` will build Docker images for running the Ruby app and building the front-end assets via Gulp. *Both containers will automatically watch for changes.*
**Note:** Using the scripts below, the Docker images will remove themselves when stopped. This is to make clean up a bit more streamlined.
1. [Install Docker](https://docs.docker.com/engine/install/) for your OS.
2. Build the docker images for Ruby and Gulp:
`bin/docker-build.sh`
3. Run the docker images:
`bin/docker-run.sh`
4. If everything is running successfully you can open your browser and go to https://localhost:9292.
### Local/Native Development
1. Install dependencies via bundler:
`bundle install`
Perform database migrations:
2. Perform database migrations:
`rake db:migrate`
Run the server in development with auto-reloading:
3. Run the server in development with auto-reloading:
`rake server:dev`
If everything is running successfully you can open your browser and go to https://localhost:9292.
4. If everything is running successfully you can open your browser and go to https://localhost:9292.
## License
This project is available under the BSD 2-Clause license.

View File

@@ -3,18 +3,22 @@ require 'bundler/setup'
namespace :db do
desc 'Run migrations'
task :migrate, [:version] do |t, args|
require "sequel/core"
require 'sequel/core'
# load configuration
require_relative 'src/config'
conf = Config.new()
Sequel.extension :migration
version = args[:version].to_i if args[:version]
Sequel.connect('sqlite://data/gamedata.db') do |db|
Sequel::Migrator.run(db, "db/migrations", target: version)
Sequel.connect(adapter: conf.get('database.adapter'), database: conf.get('database.database')) do |db|
Sequel::Migrator.run(db, 'db/migrations', target: version)
end
end
end
namespace :server do
task :start do
ENV['APP_ENV'] = 'production'
ENV['RACK_ENV'] = 'production'
system("puma")
end
@@ -24,8 +28,12 @@ namespace :server do
end
namespace :test do
task :rubocop do
system("rubocop src/")
end
task :unit do
ENV['RACK_ENV'] = 'testing'
system("rspec")
end
task :lint do
system("rubocop src/ spec/")
end
end

BIN
assets/img/app-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
assets/img/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1008 KiB

View File

@@ -1,7 +1,34 @@
$ ->
# run foundation scripts
# let us know when javascript is running.
console.log('Ready.')
lastColumn = null
ascending = true
tableHeaders = $('th')
tableHeaders.click (e) ->
column = $(this).index()
table = $(this).closest('table')
if column is lastColumn
ascending = not ascending
else
ascending = true
lastColumn = column
sortTable(table, column, ascending)
sortTable = (table, column, ascending) ->
rows = table.find('tbody tr').get()
compareFunction = (a, b) ->
res = a.cells[column].textContent.localeCompare b.cells[column].textContent
if ascending then res else -res
rows.sort compareFunction
$(rows).detach().appendTo(table.find('tbody'))
return
averageResults = (results, decimals = 2) ->
avgScore = 0
minScore = Infinity

View File

@@ -14,8 +14,20 @@ body
table
border: 1px solid #666
a
transition: color 220ms ease-in-out
#wrapper
background: white
padding: 1.5rem 2rem
border: 1px solid #bbb
border-radius: 8px
#main-nav
li
a
font-size: 1.25rem
#site-title
img
max-height: 40px

View File

@@ -1,7 +1,5 @@
# Load application config
require_relative 'src/config.rb'
$conf = Config.new(File.join(__dir__, 'config/defaults.yaml'))
root = ::File.dirname(__FILE__)
require ::File.join( root, 'src', 'app' )
require ::File.join( root, 'src', 'server' )
run GameData.new

View File

@@ -1,6 +1,6 @@
database:
adapter: 'sqlite'
database: 'data/gamedata.db'
server:
host: '0.0.0.0'
port: '9292'
testing:
minimum_results_required: 3

3
config/development.yaml Normal file
View File

@@ -0,0 +1,3 @@
database:
adapter: 'sqlite'
database: 'data/gamedata.db'

View File

@@ -3,7 +3,9 @@ directory app_dir
environment ENV.fetch('RACK_ENV', 'development')
bind 'tcp://0.0.0.0:9292'
require_relative '../src/config'
conf = Config.new()
bind "tcp://#{conf.get('server.host')}:#{conf.get('server.port')}"
workers 2
threads 1, 5

3
config/testing.yaml Normal file
View File

@@ -0,0 +1,3 @@
database:
adapter: 'sqlite'
database: 'data/gamedata_testing.db'

View File

@@ -0,0 +1,97 @@
Sequel.migration do
up do
# 1. Create benchmark_profiles
create_table(:benchmark_profiles) do
primary_key :id
String :label, null: false
String :settings, null: false
foreign_key :benchmark_id, :benchmarks, null: false, on_delete: :cascade
DateTime :created_at, default: Sequel::CURRENT_TIMESTAMP
DateTime :updated_at, default: Sequel::CURRENT_TIMESTAMP
end
# 2. Create join table (tests <-> benchmark_profiles)
create_table(:benchmark_profiles_tests) do
primary_key :id
foreign_key :test_id, :tests, null: false, on_delete: :cascade
foreign_key :benchmark_profile_id, :benchmark_profiles, null: false, on_delete: :cascade
index [:test_id, :benchmark_profile_id], unique: true
end
# 3. Add benchmark_profile_id to results
alter_table(:results) do
add_foreign_key :benchmark_profile_id, :benchmark_profiles, null: true, on_delete: :cascade
end
# 4. Migrate data from old schema
from(:benchmarks).each do |b_row|
# Create a BenchmarkProfile for this (test, benchmark) pair
bp_id = self[:benchmark_profiles].insert(
benchmark_id: b_row[:id],
label: 'Default',
settings: '{}'
)
from(:benchmarks_tests).each do |bt_row|
if bt_row[:benchmark_id] == b_row[:id]
# Link it to the test
self[:benchmark_profiles_tests].insert(
test_id: bt_row[:test_id],
benchmark_profile_id: bp_id
)
# Update results belonging to this test + benchmark pair
self[:results]
.where(test_id: bt_row[:test_id], benchmark_id: bt_row[:benchmark_id])
.update(benchmark_profile_id: bp_id)
end
end
end
# 5. Clean up old schema
alter_table(:results) do
drop_foreign_key :benchmark_id
end
drop_table(:benchmarks_tests)
end
down do
# 1. Recreate old join table
create_table(:benchmarks_tests) do
foreign_key :test_id, :tests, null: false
foreign_key :benchmark_id, :benchmarks, null: false
end
# 2. Add benchmark_id back to results
alter_table(:results) do
add_foreign_key :benchmark_id, :benchmarks, null: true
end
# 3. Restore data
from(:benchmark_profiles_tests).each do |bpt_row|
bp = self[:benchmark_profiles][id: bpt_row[:benchmark_profile_id]]
next unless bp # safety check
# Recreate old benchmarks_tests entry
self[:benchmarks_tests].insert(
test_id: bpt_row[:test_id],
benchmark_id: bp[:benchmark_id]
)
# Update results to point back to benchmark_id
self[:results]
.where(test_id: bpt_row[:test_id], benchmark_profile_id: bp[:id])
.update(benchmark_id: bp[:benchmark_id])
end
# 4. Remove new schema
alter_table(:results) do
drop_foreign_key :benchmark_profile_id
end
drop_table(:benchmark_profiles_tests)
drop_table(:benchmark_profiles)
end
end

View File

@@ -20,16 +20,23 @@ function compileCoffee() {
.pipe(gulp.dest('public/js', { sourcemaps: '.' }));
}
// Copy image files to public/img/
function copyImages() {
return gulp.src('assets/img/**/*', {encoding: false})
.pipe(gulp.dest('public/img/'));
}
// Watch files for changes
function watchFiles(cb) {
gulp.watch('assets/styles/**/*.sass', compileSass);
gulp.watch('assets/scripts/**/*.coffee', compileCoffee);
gulp.watch('assets/img/**/*', copyImages)
cb();
}
// Chain all asset builds together
function buildAssets() {
return gulp.parallel(compileSass, compileCoffee);
return gulp.parallel(compileSass, compileCoffee, copyImages);
}
// Perform initial build then watch

View File

@@ -0,0 +1,137 @@
# frozen_string_literal: true
require_relative '../spec_helper'
require_relative '../../src/models/benchmark'
RSpec.describe(BenchmarkController) do
# GET /benchmark - redirects to /benchmark/list
describe 'GET /benchmark' do
before { get '/benchmark' }
it 'Benchmark base route is a redirect.' do
expect(last_response).to(be_redirect)
end
it 'Benchmark base route is an HTML response.' do
expect(last_response['Content-Type']).to(include('text/html'))
end
it 'Benchmark base route Location header points to /benchmark/list' do
expect(last_response['Location']).to(eq("#{BASE_URL}/benchmark/list"))
end
end
# GET /benchmark/list - displays a table of benchmarks
describe 'GET /benchmark/list' do
before { get '/benchmark/list' }
it 'Benchmark list page returns 200.' do
expect(last_response).to(be_ok)
end
it 'Benchmark list page is an HTML response.' do
expect(last_response['Content-Type']).to(include('text/html'))
end
it "Benchmark list page contains 'List of benchmarks' on page." do
expect(last_response.body).to(include('List of benchmarks'))
end
end
# GET /benchmark/add - form for adding benchmark
describe 'GET /benchmark/add' do
before { get '/benchmark/add' }
it 'Benchmark add page returns 200.' do
expect(last_response).to(be_ok)
end
it 'Benchmark add page is an HTML response.' do
expect(last_response['Content-Type']).to(include('text/html'))
end
it "Benchmark add page contains 'Add new benchmark' on page." do
expect(last_response.body).to(include('Add new benchmark'))
end
end
# POST /benchmark/add - backend for adding a benchmark
describe 'POST /benchmark/add' do
before do
request_data = {
benchmark_name: 'Test Benchmark',
benchmark_scoring: 'fps',
benchmark_description: 'Benchmark for testing'
}
post '/benchmark/add', request_data
end
it 'Benchmark add POST route is a redirect.' do
expect(last_response).to(be_redirect)
end
it 'Benchmark add POST route is an HTML response.' do
expect(last_response['Content-Type']).to(include('text/html'))
end
it 'Benchmark add POST route Location header points to /benchmark/1' do
expect(last_response['Location']).to(eq("#{BASE_URL}/benchmark/1"))
end
it 'Benchmark add POST route creates new Benchmark.' do
expect(Benchmark.count).to(eq(1))
end
it 'Benchmark add POST route created benchmark has name.' do
expect(Benchmark.first.name).to(eq('Test Benchmark'))
end
it 'Benchmark add POST route created benchmark has scoring type.' do
expect(Benchmark.first.scoring).to(eq('fps'))
end
it 'Benchmark add POST route created benchmark has description.' do
expect(Benchmark.first.description).to(eq('Benchmark for testing'))
end
end
# GET /benchmark/:benchmark_id - page for viewing a benchmark model
describe 'GET /benchmark/:benchmark_id' do
before do
@benchmark = Benchmark.create(name: 'Test Benchmark', scoring: 'fps')
get "/benchmark/#{@benchmark.id}"
end
it 'Benchmark view page returns 200.' do
expect(last_response).to(be_ok)
end
it 'Benchmark view page is an HTML response' do
expect(last_response['Content-Type']).to(include('text/html'))
end
it 'Benchmark view page contains "Add new benchmark" on page.' do
expect(last_response.body).to(include("#{@benchmark.name}"))
end
end
# GET /benchmark/:benchmark_id/edit - page for editing a benchmark model
describe 'GET /benchmark/:benchmark_id/edit' do
before do
@benchmark = Benchmark.create(name: 'Test Benchmark', scoring: 'fps')
get "/benchmark/#{@benchmark.id}/edit"
end
it 'Benchmark edit page returns 200.' do
expect(last_response).to(be_ok)
end
it 'Benchmark edit page is an HTML response' do
expect(last_response['Content-Type']).to(include('text/html'))
end
it 'Benchmark edit page contains "Editing: <benchmark name>" on page.' do
expect(last_response.body).to(include("Editing: #{@benchmark.name}"))
end
end
end

View File

@@ -0,0 +1,132 @@
# frozen_string_literal: true
require_relative '../spec_helper'
require_relative '../../src/models/hardware'
RSpec.describe(HardwareController) do
# GET /hardware - redirects to /hardware/list
describe 'GET /hardware' do
before { get '/hardware' }
it 'Hardware base route is a redirect' do
expect(last_response).to(be_redirect)
end
it 'Hardware base route is an HTML response' do
expect(last_response['Content-Type']).to(include('text/html'))
end
it 'Hardware base route Location header points to /hardware/list' do
expect(last_response['Location']).to(eq("#{BASE_URL}/hardware/list"))
end
end
# GET /hardware/list - displays a table of hardwares
describe 'GET /hardware/list' do
before { get '/hardware/list' }
it 'Hardware list page returns 200.' do
expect(last_response).to(be_ok)
end
it 'Hardware list page is an HTML response' do
expect(last_response['Content-Type']).to(include('text/html'))
end
it "Hardware list page contains 'List of hardware' on page." do
expect(last_response.body).to(include('List of hardware'))
end
end
# GET /hardware/add - form for adding hardware
describe 'GET /hardware/add' do
before { get '/hardware/add' }
it 'Hardware add page returns 200.' do
expect(last_response).to(be_ok)
end
it 'Hardware add page is an HTML response' do
expect(last_response['Content-Type']).to(include('text/html'))
end
it "Hardware add page contains 'Add new hardware' on page." do
expect(last_response.body).to(include('Add new hardware'))
end
end
# POST /hardware/add - backend for adding a hardware component
describe 'POST /hardware/add' do
before do
request_data = {
hardware_name: 'Test Hardware',
hardware_type: 'gpu'
}
post '/hardware/add', request_data
end
it 'Hardware add POST route is a redirect.' do
expect(last_response).to(be_redirect)
end
it 'Hardware add POST route is an HTML response.' do
expect(last_response['Content-Type']).to(include('text/html'))
end
it 'Hardware add POST route Location header points to /hardware/1' do
expect(last_response['Location']).to(eq("#{BASE_URL}/hardware/1"))
end
it 'Hardware add POST route creates new Hardware.' do
expect(Hardware.count).to(eq(1))
end
it 'Hardware add POST route created hardware has name.' do
expect(Hardware.first.name).to(eq('Test Hardware'))
end
it 'Hardware add POST route created hardware has type.' do
expect(Hardware.first.type).to(eq('gpu'))
end
end
# GET /hardware/:hardware_id - page for viewing a hardware model
describe 'GET /hardware/:hardware_id' do
before do
@hardware = Hardware.create(name: 'Test Hardware', type: 'gpu')
get "/hardware/#{@hardware.id}"
end
it 'Hardware view page returns 200.' do
expect(last_response).to(be_ok)
end
it 'Hardware view page is an HTML response' do
expect(last_response['Content-Type']).to(include('text/html'))
end
it 'Hardware view page contains "Add new hardware" on page.' do
expect(last_response.body).to(include("#{@hardware.name}"))
end
end
# GET /hardware/:hardware_id/edit - page for editing a hardware model
describe 'GET /hardware/:hardware_id/edit' do
before do
@hardware = Hardware.create(name: 'Test Hardware', type: 'gpu')
get "/hardware/#{@hardware.id}/edit"
end
it 'Hardware edit page returns 200.' do
expect(last_response).to(be_ok)
end
it 'Hardware edit page is an HTML response' do
expect(last_response['Content-Type']).to(include('text/html'))
end
it 'Hardware edit page contains "Editing: <hardware name>" on page.' do
expect(last_response.body).to(include("Editing: #{@hardware.name}"))
end
end
end

View File

@@ -0,0 +1,25 @@
# frozen_string_literal: true
require_relative '../spec_helper'
RSpec.describe(IndexController) do
describe 'GET /' do
before { get '/' }
it 'Dashboard returns 200.' do
expect(last_response).to(be_ok)
end
it 'Dashboard is an HTML response' do
expect(last_response['Content-Type']).to(include('text/html'))
end
it "Dashboard contains 'Game Data' on page (nav bar should be loaded)." do
expect(last_response.body).to(include('Game Data'))
end
it "Dashboard contains 'Ruby version' on page (footer should be loaded)." do
expect(last_response.body).to(include('Ruby version'))
end
end
end

View File

@@ -0,0 +1,21 @@
# frozen_string_literal: true
require_relative '../spec_helper'
RSpec.describe(ReportsController) do
describe 'GET /report' do
before { get '/report' }
it 'Reports page returns 200.' do
expect(last_response).to(be_ok)
end
it 'Reports page is an HTML response' do
expect(last_response['Content-Type']).to(include('text/html'))
end
it 'Reports page contains "Generate report" on page.' do
expect(last_response.body).to(include('Generate report'))
end
end
end

View File

@@ -0,0 +1,165 @@
# frozen_string_literal: true
require_relative '../spec_helper'
RSpec.describe(TestController) do
# GET /test - redirects to /test/list
describe 'GET /test' do
before { get '/test' }
it 'Test base route is a redirect' do
expect(last_response).to(be_redirect)
end
it 'Test base route is an HTML response' do
expect(last_response['Content-Type']).to(include('text/html'))
end
it 'Test base route Location header points to /test/list' do
expect(last_response['Location']).to(eq("#{BASE_URL}/test/list"))
end
end
# GET /test/list - displays a table of tests
describe 'GET /test/list' do
before { get '/test/list' }
it 'Test list page returns 200.' do
expect(last_response).to(be_ok)
end
it 'Test list page is an HTML response' do
expect(last_response['Content-Type']).to(include('text/html'))
end
it "Test list page contains 'List of tests' on page." do
expect(last_response.body).to(include('List of tests'))
end
end
# GET /test/add - form for adding test
describe 'GET /test/add' do
before { get '/test/add' }
it 'Test add page returns 200.' do
expect(last_response).to(be_ok)
end
it 'Test add page is an HTML response' do
expect(last_response['Content-Type']).to(include('text/html'))
end
it "Test add page contains 'Add new test' on page." do
expect(last_response.body).to(include('Add new test'))
end
end
# POST /test/add - backend for adding a test
describe 'POST /test/add' do
before do
@hardware = Hardware.create(name: 'Test Hardware', type: 'gpu')
@benchmark = Benchmark.create(name: 'Test Benchmark', scoring: 'fps')
request_data = {
test_name: 'Test Test',
test_hardware: @hardware.id,
'test_benchmarks[]': [@benchmark.id],
test_description: 'Test for testing'
}
post '/test/add', request_data
end
it 'Test add POST route is a redirect.' do
expect(last_response).to(be_redirect)
end
it 'Test add POST route is an HTML response.' do
expect(last_response['Content-Type']).to(include('text/html'))
end
it 'Test add POST route Location header points to /test/1' do
expect(last_response['Location']).to(eq("#{BASE_URL}/test/1"))
end
it 'Test add POST route creates new Test.' do
expect(Test.count).to(eq(1))
end
it 'Test add POST route created test has name.' do
expect(Test.first.name).to(eq('Test Test'))
end
it 'Test add POST route created test has hardware.' do
expect(Test.first.hardware.id).to(eq(@hardware.id))
end
it 'Test add POST route created test has benchmarks.' do
expect(Test.first.benchmarks.length).to(eq(1))
end
it 'Test add POST route created test\'s benchmark can be read.' do
expect(Test.first.benchmarks[0].id).to(eq(@benchmark.id))
end
it 'Test add POST route created test has description.' do
expect(Test.first.description).to(eq('Test for testing'))
end
end
# GET /test/:test_id - page for viewing a test model
describe 'GET /test/:test_id' do
before do
@hardware = Hardware.create(name: 'Test Hardware', type: 'gpu')
@benchmark = Benchmark.create(name: 'Test Benchmark', scoring: 'fps')
@test = Test.create(
name: 'Test Test',
hardware_id: @hardware.id,
description: 'Test for testing'
)
@test.add_benchmark(@benchmark)
get "/test/#{@test.id}"
end
it 'Test view page returns 200.' do
expect(last_response).to(be_ok)
end
it 'Test view page is an HTML response' do
expect(last_response['Content-Type']).to(include('text/html'))
end
it 'Test view page contains test name on page.' do
expect(last_response.body).to(include("#{@test.name}"))
end
it 'Test view page contains hardware name on page.' do
expect(last_response.body).to(include("#{@hardware.name}"))
end
end
# GET /test/:test_id/edit - page for editing a test model
describe 'GET /test/:test_id/edit' do
before do
@hardware = Hardware.create(name: 'Test Hardware', type: 'gpu')
@benchmark = Benchmark.create(name: 'Test Benchmark', scoring: 'fps')
@test = Test.create(
name: 'Test Test',
hardware_id: @hardware.id,
description: 'Test for testing'
)
@test.add_benchmark(@benchmark)
get "/test/#{@test.id}/edit"
end
it 'Test edit page returns 200.' do
expect(last_response).to(be_ok)
end
it 'Test edit page is an HTML response' do
expect(last_response['Content-Type']).to(include('text/html'))
end
it 'Test edit page contains "Editing: <test name>" on page.' do
expect(last_response.body).to(include("Editing: #{@test.name}"))
end
end
end

View File

@@ -0,0 +1,27 @@
# frozen_string_literal: true
require_relative '../spec_helper'
RSpec.describe(Benchmark) do
describe 'Benchmark Creation' do
it 'Benchmark creation updates model count.' do
expect do
described_class.create(name: 'Test Benchmark', scoring: 'fps')
end.to(change(described_class, :count).by(1))
end
end
describe 'Benchmark Read' do
before { described_class.create(name: 'Test Benchmark', scoring: 'fps') }
it 'Benchmark model has name.' do
bench = described_class.first()
expect(bench.name).to(eq('Test Benchmark'))
end
it 'Benchmark model has scoring.' do
bench = described_class.first()
expect(bench.scoring).to(eq('fps'))
end
end
end

View File

@@ -0,0 +1,25 @@
# frozen_string_literal: true
require_relative '../spec_helper'
RSpec.describe(Hardware) do
describe 'Hardware Creation' do
it 'Hardware creation updates model count.' do
expect { described_class.create(name: 'Test Hardware', type: 'gpu') }.to(change(described_class, :count).by(1))
end
end
describe 'Hardware Read' do
before { described_class.create(name: 'Test Hardware', type: 'gpu') }
it 'Hardware model has name.' do
hardware = described_class.first()
expect(hardware.name).to(eq('Test Hardware'))
end
it 'Hardware model has scoring.' do
hardware = described_class.first()
expect(hardware.type).to(eq('gpu'))
end
end
end

View File

@@ -0,0 +1,36 @@
# frozen_string_literal: true
require_relative '../spec_helper'
RSpec.describe(Test) do
describe 'Test Creation' do
it 'Test creation updates model count.' do
expect { described_class.create(name: 'Test Test') }.to(change(described_class, :count).by(1))
end
end
describe 'Test Read' do
before do
described_class.create(name: 'Test Test')
end
it 'Test model has name.' do
tst = described_class.first()
expect(tst.name).to(eq('Test Test'))
end
end
describe 'Test one-to-many association with Hardware' do
it 'Test model has Hardware associated with it.' do
hardware = Hardware.create(name: 'Test Hardware', type: 'gpu')
tst = described_class.create(name: 'Test Test', hardware_id: hardware.id)
expect(tst.hardware).to(eq(hardware))
end
it 'Test model\'s hardware has name set.' do
hardware = Hardware.create(name: 'Test Hardware', type: 'gpu')
tst = described_class.create(name: 'Test Test', hardware_id: hardware.id)
expect(tst.hardware.name).to(eq('Test Hardware'))
end
end
end

35
spec/spec_helper.rb Normal file
View File

@@ -0,0 +1,35 @@
# frozen_string_literal: true
ENV['APP_ENV'] = 'test'
require_relative '../src/server'
require 'rspec'
require 'rack/test'
require 'database_cleaner/sequel'
# setting this here so all redirect tests can reference the same base URL
BASE_URL = 'http://example.org'
module RSpecMixin
include Rack::Test::Methods
def app
GameData
end
end
RSpec.configure do |config|
config.include(RSpecMixin)
config.before(:suite) do
DatabaseCleaner.strategy = :transaction
end
config.around do |suite|
DatabaseCleaner.cleaning do
suite.run
end
end
end

View File

@@ -1,41 +0,0 @@
# frozen_string_literal: true
require 'sinatra/base'
require 'sequel'
require 'sqlite3'
# Load the Sequel timestamps plugin
Sequel::Model.plugin(:timestamps)
# Initialize Sequel gem for database actions
DB = Sequel.connect(adapter: $conf.get('database.adapter'), database: $conf.get('database.database'))
# Load in routes (must happen after Sequel is loaded!)
require_relative 'routes/api1'
require_relative 'routes/benchmark'
require_relative 'routes/hardware'
require_relative 'routes/index'
require_relative 'routes/reports'
require_relative 'routes/result'
require_relative 'routes/test'
# GameData - main app that gets launched
# - inherits from Sinatra::Base to instantiate the server
# - sets up some base app configuration
# - registers route classes with the base app
class GameData < Sinatra::Base
enable :sessions
# Set up static file serving
enable :static
set :public_folder, File.join(__dir__, '/../public')
use IndexRoutes
use HardwareRoutes
use BenchmarkRoutes
use TestRoutes
use ResultRoutes
use ReportsRoutes
use APIv1Routes
end

View File

@@ -2,6 +2,6 @@
module AppInfo
VERSION = '0.1.1'
VERSION = '0.2.1'
end

View File

@@ -6,8 +6,9 @@ require 'yaml'
class Config
DEFAULT_CONFIG = 'config/defaults.yaml'
ENVIRONMENT_CONFIG = ENV.fetch('RACK_ENV', 'development')
def initialize(config_path)
def initialize(config_path = "config/#{ENVIRONMENT_CONFIG}.yaml")
@data = YAML.load_file(DEFAULT_CONFIG)
# merge in user-defined configuration if it exists

View File

@@ -2,13 +2,13 @@
require 'sinatra/json'
require_relative '../server'
require_relative 'base_controller'
require_relative '../models/benchmark'
require_relative '../models/test'
require_relative '../models/result'
# /api/v1 routes
class APIv1Routes < Server
class APIv1Controller < BaseController
get '/api/v1/benchmark/details' do
benchmark_id = params[:benchmark_id]

View File

@@ -0,0 +1,15 @@
# frozen_string_literal: true
require 'sinatra/base'
# BaseController - base modular Sinatra app class
class BaseController < Sinatra::Base
# Register view helpers
require_relative '../helpers'
helpers Helpers
# Set up our view engine
set :views, File.join(settings.root, '/../../views')
end

View File

@@ -1,12 +1,16 @@
# frozen_string_literal: true
require_relative '../server'
require_relative 'base_controller'
require_relative '../models/benchmark'
# /benchmark routes
class BenchmarkRoutes < Server
class BenchmarkController < BaseController
get '/benchmark' do
redirect('/benchmark/list')
end
get '/benchmark/list' do
benchmarks = Benchmark.reverse(:updated_at).limit(10).all()
erb :'benchmark/index', locals: {

View File

@@ -1,13 +1,17 @@
# frozen_string_literal: true
require_relative '../server'
require_relative 'base_controller'
require_relative '../models/hardware'
require_relative '../models/benchmark'
# /hardware routes
class HardwareRoutes < Server
class HardwareController < BaseController
get '/hardware' do
redirect('/hardware/list')
end
get '/hardware/list' do
hardware = Hardware.reverse(:updated_at).limit(10).all()
erb :'hardware/index', locals: {

View File

@@ -1,10 +1,11 @@
# frozen_string_literal: true
require_relative '../server'
require_relative 'base_controller'
require_relative '../models/test'
require_relative '../models/benchmark_profile'
# / (top-level) routes
class IndexRoutes < Server
class IndexController < BaseController
get '/' do
tests = Test.reverse(:updated_at).limit(10).all()

View File

@@ -2,13 +2,13 @@
require 'sinatra/json'
require_relative '../server'
require_relative 'base_controller'
require_relative '../models/benchmark'
require_relative '../models/result'
require_relative '../models/test'
# /reports routes
class ReportsRoutes < Server
class ReportsController < BaseController
get '/report' do
benchmarks = Benchmark.order(:name).all()

View File

@@ -1,10 +1,10 @@
# frozen_string_literal: true
require_relative '../server'
require_relative 'base_controller'
require_relative '../models/result'
# /result routes
class ResultRoutes < Server
class ResultController < BaseController
post '/result/add' do
result_minimum = params[:result_minimum] if params.key?(:result_minimum)

View File

@@ -1,14 +1,18 @@
# frozen_string_literal: true
require_relative '../server'
require_relative 'base_controller'
require_relative '../models/benchmark'
require_relative '../models/hardware'
require_relative '../models/test'
# /test routes
class TestRoutes < Server
class TestController < BaseController
get '/test' do
redirect('/test/list')
end
get '/test/list' do
tests = Test.reverse(:updated_at).limit(10).all()
erb :'test/index', locals: {

View File

@@ -3,7 +3,6 @@
# Benchmark - database model for PC benchmarks
class Benchmark < Sequel::Model
many_to_many :tests
one_to_many :results
one_to_many :benchmark_profiles
end

View File

@@ -0,0 +1,14 @@
# frozen_string_literal: true
# BenchmarkProfile - database model for benchmark settings profile
class BenchmarkProfile < Sequel::Model
many_to_one :benchmark
many_to_many :tests
one_to_many :results
def display_name
"#{benchmark.name} @ #{label}"
end
end

View File

@@ -4,7 +4,7 @@
class Result < Sequel::Model
many_to_one :test
many_to_one :benchmark
many_to_one :benchmark_profile
def formatted_score
return @avg_score

View File

@@ -5,10 +5,10 @@ class Test < Sequel::Model
one_to_many :result
many_to_one :hardware
many_to_many :benchmarks
many_to_many :benchmark_profiles
def benchmark?(benchmark_id)
return benchmarks_dataset.where(Sequel[:benchmarks][:id] => benchmark_id).any?
return benchmark_profiles_dataset.where(Sequel[:benchmark][:id] => benchmark_id).any?
end
end

45
src/server.rb Executable file → Normal file
View File

@@ -1,15 +1,46 @@
# frozen_string_literal: true
require 'sinatra/base'
require 'sequel'
require 'sqlite3'
# Server - base modular Sinatra app class
class Server < Sinatra::Base
require_relative 'config'
# Register view helpers
require_relative 'helpers'
helpers Helpers
# Load configuration from environment config file
$conf = Config.new()
# Set up our view engine
set :views, File.join(settings.root, '/../views')
# Load the Sequel timestamps plugin
Sequel::Model.plugin(:timestamps)
# Initialize Sequel gem for database actions
DB = Sequel.connect(adapter: $conf.get('database.adapter'), database: $conf.get('database.database'))
# Load in routes (must happen after Sequel is loaded!)
require_relative 'controllers/api1'
require_relative 'controllers/benchmark'
require_relative 'controllers/hardware'
require_relative 'controllers/index'
require_relative 'controllers/reports'
require_relative 'controllers/result'
require_relative 'controllers/test'
# GameData - main app that gets launched
# - inherits from Sinatra::Base to instantiate the server
# - sets up some base app configuration
# - registers route classes with the base app
class GameData < Sinatra::Base
enable :sessions
# Set up static file serving
enable :static
set :public_folder, File.join(__dir__, '/../public')
use IndexController
use HardwareController
use BenchmarkController
use TestController
use ResultController
use ReportsController
use APIv1Controller
end

View File

@@ -21,29 +21,31 @@
<div class="row">
<div class="col-12">
<h3 class="mb-3">Tests using this benchmark:</h3>
<h3 class="mb-3">Profiles created for this benchmark:</h3>
<% if benchmark.tests.length > 0 %>
<% if benchmark.benchmark_profiles.length > 0 %>
<table class="table table-hover table-responsive">
<thead class="table-light">
<tr>
<th>Test title</th>
<th>Benchmarks</th>
<th>Profile Label</th>
<th>Tests Linked</th>
<th>Created at</th>
<th>Last updated</th>
</tr>
</thead>
<tbody>
<% benchmark.tests.each do |t| %>
<% benchmark.benchmark_profiles.each do |bp| %>
<tr>
<td><a href="/test/<%= t.id %>"><%= t.name %></a></td>
<td><%= t.benchmarks.length %></td>
<td><%= t.updated_at %></td>
<td><%= bp.display_name %></td>
<td><%= bp.tests.length %></td>
<td><%= bp.created_at %></td>
<td><%= bp.updated_at %></td>
</tr>
<% end %>
</tbody>
</table>
<% else %>
<p>There are no tests associated with this benchmark.</p>
<p>There are no profiles associated with this benchmark.</p>
<% end %>
</div>
</div>

View File

@@ -8,16 +8,16 @@
<table class="table table-hover table-responsive">
<thead class="table-light">
<tr>
<th>Test name</th>
<th># Benchmarks</th>
<th>Last Updated</th>
<th data-sort="name">Test name</th>
<th data-sort="benchmark-count"># Benchmarks</th>
<th data-sort="updated'">Last Updated</th>
</tr>
</thead>
<tbody>
<% tests.each do |t| %>
<tr>
<td><a href="/test/<%= t.id %>"><%= t.name %></a></td>
<td><%= t.benchmarks.length %></td>
<td><%= t.benchmark_profiles.length %></td>
<td><%= t.updated_at %></td>
</tr>
<% end %>

View File

@@ -4,6 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="shortcut icon" href="/img/favicon.png">
<title><%= title %> | Game Data</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.7/css/bootstrap.min.css">
<link rel="stylesheet" href="/css/rimmington.css">

View File

@@ -1,6 +1,9 @@
<div id="main-nav" class="navbar navbar-expand-md bg-dark border-bottom border-body mb-3" data-bs-theme="dark">
<div class="container-fluid">
<a class="navbar-brand mb-0 h1" href="#">Game Data</a>
<a id="site-title" class="navbar-brand mb-0 h1" href="#">
Game Data
<img src="/img/app-logo.png" alt="">
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>

View File

@@ -1,3 +1,9 @@
<div class="row">
<div class="col-12">
<h1>Generate report</h1>
</div>
</div>
<div class="row">
<form class="col-12" action="/reports" method="post">
<div class="row mb-3">