From 1976ec6ccac9354ae20f11de53faab3f5ccb659f Mon Sep 17 00:00:00 2001 From: JonJagger Date: Wed, 20 May 2026 15:27:04 +0100 Subject: [PATCH 1/4] Expose diff_lines_files/diff_summary_files in API, add parallel tests, fix saver container lookup - Route diff_lines_files and diff_summary_files in app.rb - Run tests in parallel via parallelize_me! with Etc.nprocessors workers; TIMINGS_LOCK mutex guards the shared @@timings hash - copy_in_saver_test_data was grepping all running containers for "saver", which matched unrelated containers from other projects; now uses CYBER_DOJO_SAVER_CONTAINER_NAME directly, set in echo_env_vars.sh and wired into docker-compose.yml Co-Authored-By: Claude Sonnet 4.6 --- bin/echo_env_vars.sh | 1 + bin/lib.sh | 2 +- docker-compose.yml | 1 + source/server/app.rb | 6 ++++-- test/client/lib/id58_test_base.rb | 8 +++++++- test/server/lib/id58_test_base.rb | 8 +++++++- 6 files changed, 21 insertions(+), 5 deletions(-) diff --git a/bin/echo_env_vars.sh b/bin/echo_env_vars.sh index 48bc3c02..5ed6e790 100644 --- a/bin/echo_env_vars.sh +++ b/bin/echo_env_vars.sh @@ -28,6 +28,7 @@ echo_env_vars() # echo CYBER_DOJO_DIFFER_CLIENT_CONTAINER_NAME=test_differ_client echo CYBER_DOJO_DIFFER_SERVER_CONTAINER_NAME=test_differ_server + echo CYBER_DOJO_SAVER_CONTAINER_NAME=test_differ_saver # local -r AWS_ACCOUNT_ID=244531986313 local -r AWS_REGION=eu-central-1 diff --git a/bin/lib.sh b/bin/lib.sh index 7e329427..238f2fde 100644 --- a/bin/lib.sh +++ b/bin/lib.sh @@ -41,7 +41,7 @@ exit_non_zero() copy_in_saver_test_data() { - local -r SAVER_CID=$(docker ps --filter status=running --format '{{.Names}}' | grep "saver") + local -r SAVER_CID="${CYBER_DOJO_SAVER_CONTAINER_NAME}" local -r SRC_PATH=${ROOT_DIR}/test/server/data/cyber-dojo local -r DEST_PATH=/cyber-dojo # You cannot docker cp to a tmpfs, so tar-piping instead... diff --git a/docker-compose.yml b/docker-compose.yml index 606f9ab1..9077d3b3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -49,6 +49,7 @@ services: saver: image: ${CYBER_DOJO_SAVER_IMAGE}:${CYBER_DOJO_SAVER_TAG} + container_name: ${CYBER_DOJO_SAVER_CONTAINER_NAME} user: saver ports: [ "${CYBER_DOJO_SAVER_PORT}:${CYBER_DOJO_SAVER_PORT}" ] env_file: [ .env ] diff --git a/source/server/app.rb b/source/server/app.rb index b373604b..7ff4a278 100644 --- a/source/server/app.rb +++ b/source/server/app.rb @@ -7,6 +7,8 @@ class App < AppBase get_json(:ready, Prober) get_json(:sha, Prober) - get_json(:diff_lines, Differ) - get_json(:diff_summary, Differ) + get_json(:diff_lines, Differ) + get_json(:diff_summary, Differ) + get_json(:diff_lines_files, Differ) + get_json(:diff_summary_files, Differ) end diff --git a/test/client/lib/id58_test_base.rb b/test/client/lib/id58_test_base.rb index 01ba0f08..68556689 100644 --- a/test/client/lib/id58_test_base.rb +++ b/test/client/lib/id58_test_base.rb @@ -1,8 +1,11 @@ require 'English' +require 'etc' require 'minitest/autorun' require 'minitest/reporters' require_relative 'slim_json_reporter' +Minitest.parallel_executor = Minitest::Parallel::Executor.new(Etc.nprocessors) + reporters = [ Minitest::Reporters::DefaultReporter.new, Minitest::Reporters::SlimJsonReporter.new, @@ -21,9 +24,12 @@ def initialize(arg) super end + parallelize_me! + @@args = (ARGV.sort.uniq - ['--']) # eg 2m4 @@seen_ids = [] @@timings = {} + TIMINGS_LOCK = Mutex.new def self.test(id58, *lines, &test_block) src = test_block.source_location @@ -42,7 +48,7 @@ def self.test(id58, *lines, &test_block) t1 = Time.now instance_eval(&test_block) t2 = Time.now - @@timings["#{id58}:#{src_file}:#{src_line}:#{name58}"] = (t2 - t1) + TIMINGS_LOCK.synchronize { @@timings["#{id58}:#{src_file}:#{src_line}:#{name58}"] = (t2 - t1) } ensure puts $ERROR_INFO.message unless $ERROR_INFO.nil? id58_teardown diff --git a/test/server/lib/id58_test_base.rb b/test/server/lib/id58_test_base.rb index 01ba0f08..68556689 100644 --- a/test/server/lib/id58_test_base.rb +++ b/test/server/lib/id58_test_base.rb @@ -1,8 +1,11 @@ require 'English' +require 'etc' require 'minitest/autorun' require 'minitest/reporters' require_relative 'slim_json_reporter' +Minitest.parallel_executor = Minitest::Parallel::Executor.new(Etc.nprocessors) + reporters = [ Minitest::Reporters::DefaultReporter.new, Minitest::Reporters::SlimJsonReporter.new, @@ -21,9 +24,12 @@ def initialize(arg) super end + parallelize_me! + @@args = (ARGV.sort.uniq - ['--']) # eg 2m4 @@seen_ids = [] @@timings = {} + TIMINGS_LOCK = Mutex.new def self.test(id58, *lines, &test_block) src = test_block.source_location @@ -42,7 +48,7 @@ def self.test(id58, *lines, &test_block) t1 = Time.now instance_eval(&test_block) t2 = Time.now - @@timings["#{id58}:#{src_file}:#{src_line}:#{name58}"] = (t2 - t1) + TIMINGS_LOCK.synchronize { @@timings["#{id58}:#{src_file}:#{src_line}:#{name58}"] = (t2 - t1) } ensure puts $ERROR_INFO.message unless $ERROR_INFO.nil? id58_teardown From f8fcad57a378c87f5bea6d970cc29bcfbac10083 Mon Sep 17 00:00:00 2001 From: JonJagger Date: Wed, 20 May 2026 15:54:50 +0100 Subject: [PATCH 2/4] Remove unused ENV['ID58'] write that was unsafe under parallel test execution Nothing in the repo reads ENV['ID58'], so the write served no purpose. With parallelize_me! in place, concurrent threads racing to set it would be a latent hazard. Co-Authored-By: Claude Sonnet 4.6 --- test/client/lib/id58_test_base.rb | 1 - test/server/lib/id58_test_base.rb | 1 - 2 files changed, 2 deletions(-) diff --git a/test/client/lib/id58_test_base.rb b/test/client/lib/id58_test_base.rb index 68556689..46bd91ac 100644 --- a/test/client/lib/id58_test_base.rb +++ b/test/client/lib/id58_test_base.rb @@ -40,7 +40,6 @@ def self.test(id58, *lines, &test_block) name58 = lines.join(' ') execute_around = lambda { - ENV['ID58'] = id58 @_id58 = id58 @_name58 = name58 id58_setup diff --git a/test/server/lib/id58_test_base.rb b/test/server/lib/id58_test_base.rb index 68556689..46bd91ac 100644 --- a/test/server/lib/id58_test_base.rb +++ b/test/server/lib/id58_test_base.rb @@ -40,7 +40,6 @@ def self.test(id58, *lines, &test_block) name58 = lines.join(' ') execute_around = lambda { - ENV['ID58'] = id58 @_id58 = id58 @_name58 = name58 id58_setup From b40cdda3ad774ddad9b8434c0fe5163c76692ad9 Mon Sep 17 00:00:00 2001 From: JonJagger Date: Wed, 20 May 2026 16:02:52 +0100 Subject: [PATCH 3/4] Document diff_lines_files and diff_summary_files in API docs and README Full self-contained documentation for both new endpoints in docs/api.md, with all the same examples as diff_lines. README API index updated to include links to the new endpoints. Co-Authored-By: Claude Sonnet 4.6 --- README.md | 2 + docs/api.md | 135 +++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 136 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d50194f2..1b365154 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,8 @@ $ make demo * [GET alive](docs/api.md#get-alive) * [GET ready](docs/api.md#get-ready) * [GET sha](docs/api.md#get-sha) +* [GET diff_lines_files(was_files,now_files)](docs/api.md#get-diff_lines_fileswas_filesnow_files) +* [GET diff_summary_files(was_files,now_files)](docs/api.md#get-diff_summary_fileswas_filesnow_files) * [GET diff_lines(id,was_index,now_index)](docs/api.md#get-diff_linesidwas_indexnow_index) * [GET diff_summary(id,was_index,now_index)](docs/api.md#get-diff_summaryidwas_indexnow_index) diff --git a/docs/api.md b/docs/api.md index 2cd850e2..7d7d8eb4 100644 --- a/docs/api.md +++ b/docs/api.md @@ -3,7 +3,7 @@ - - - - ## GET diff_lines(id,was_index,now_index) A diff of two sets of files (designated with **was_index** and **now_index**) from the kata designated with **id**. -Also includes unchanged files and the content of files renamed but with identical content. +Also handles unchanged files and files renamed but with identical content. - parameters * **id:String** the id of the kata. * **was_index:Integer** the test-submission index of the first set of files. @@ -108,6 +108,139 @@ Also includes unchanged files and the content of files renamed but with identica ] ``` +- - - - +## GET diff_lines_files(was_files,now_files) +A diff of two sets of files supplied directly as parameters. +Also handles unchanged files and files renamed but with identical content. +- parameters + * **was_files:Hash** a Hash of filename to content Strings for the first set of files. + * **now_files:Hash** a Hash of filename to content Strings for the second set of files. + * eg + ```json + { + "was_files": { "hiker.h": "#ifndef HIKER_INCLUDED\n#define WIBBLE\n" }, + "now_files": { "hiker.h": "#ifndef HIKER_INCLUDED\n#define HIKER_INCLUDED\n" } + } + ``` +- returns an Array of Hashes with each Hash being the diff of a single file. Each Hash has the following keys: + * "type" - one of the Strings [ "created", "deleted", "renamed", "changed", "unchanged" ]. + * "old_filename" - the String name of the **was_files** file, unless "type" is "created" in which case **null**. + * "new_filename" - the String name of the **now_files** file, unless "type" is "deleted" in which case **null**. + * "lines" - an Array of Hashes, each Hash detailing an "added", "deleted", or "same" line, or + a "section" marker before a diff-chunk. + * "line_counts" - a Hash with three entries: + - "added" - the number of lines added to **now_files**'s "new_filename" file. + - "deleted" - the number of lines deleted from **was_files**'s "old_filename" file. + - "same" - the number of lines identical in both **now_files**'s "new_filename" and **was_files**'s "old_filename" file. + * + * eg a created file, which always has a single "section" marker. + ```json + [ + { + "type": "created", + "old_filename": null, + "new_filename": "the.created.filename", + "lines": + [ + { "type": "section", "index": 0 }, + { "type": "added", "line": "this file", "number": 1 }, + { "type": "added", "line": "is new", "number": 2 } + ], + "line_counts": { "added":2, "deleted":0, "same":0 } + } + , + ... + ] + ``` + * eg a deleted file, which always has a single "section" marker. + ```json + [ + { + "type": "deleted", + "old_filename": "the.deleted.filename", + "new_filename": null, + "lines": + [ + { "type": "section", "index": 0 }, + { "type": "deleted", "line": "this file", "number": 1 }, + { "type": "deleted", "line": "had 2 lines", "number": 2 } + ], + "line_counts": { "added":0, "deleted":2, "same":0 } + } + , + ... + ] + ``` + * eg a renamed file with identical content, which always has zero "section" markers. + ```json + [ + { + "type": "renamed", + "old_filename": "the.old.filename", + "new_filename": "the.new.filename", + "lines": + [ + { "type": "same", "line": "this file has", "number": 1 }, + { "type": "same", "line": "changed its name", "number": 2 } + { "type": "same", "line": "but not its contents", "number": 3 } + ], + "line_counts": { "added":0, "deleted":0, "same":3 } + } + , + ... + ] + ``` + * eg a changed file with two diff-chunks. + ```json + [ + { + "type": "changed", + "old_filename": "hiker.h", + "new_filename": "hiker.h", + "lines": + [ + { "type": "same", "line": "#ifndef HIKER_INCLUDED", "number": 1 }, + { "type": "section", "index": 0 }, + { "type": "deleted", "line": "#define WIBBLE", "number": 2 }, + { "type": "added", "line": "#define HIKER_INCLUDED", "number": 2 }, + { "type": "same", "line": "", "number": 3 }, + { "type": "section", "index": 1 }, + { "type": "deleted", "line": "struct wibble", "number": 3 }, + { "type": "added", "line": "struct hiker", "number": 3 }, + { "type": "same", "line": "{", "number": 4 }, + { "type": "same", "line": "};", "number": 5 }, + { "type": "same", "line": "#endif", "number": 6 }, + ], + "line_counts": { "added":2, "deleted":2, "same":5 } + } + , + ... + ] + ``` + +- - - - +## GET diff_summary_files(was_files,now_files) +A diff of two sets of files supplied directly as parameters. +Also handles unchanged files and files renamed but with identical content. +- parameters + * **was_files:Hash** a Hash of filename to content Strings for the first set of files. + * **now_files:Hash** a Hash of filename to content Strings for the second set of files. + * eg + ```json + { + "was_files": { "hiker.h": "#ifndef HIKER_INCLUDED\n#define WIBBLE\n" }, + "now_files": { "hiker.h": "#ifndef HIKER_INCLUDED\n#define HIKER_INCLUDED\n" } + } + ``` +- returns an Array of Hashes with each Hash being the diff of a single file. Each Hash has the following keys: + * "type" - one of the Strings [ "created", "deleted", "renamed", "changed", "unchanged" ]. + * "old_filename" - the String name of the **was_files** file, unless "type" is "created" in which case **null**. + * "new_filename" - the String name of the **now_files** file, unless "type" is "deleted" in which case **null**. + * "line_counts" - a Hash with three entries: + - "added" - the number of lines added to **now_files**'s "new_filename" file. + - "deleted" - the number of lines deleted from **was_files**'s "old_filename" file. + - "same" - the number of lines identical in both **now_files**'s "new_filename" and **was_files**'s "old_filename" file. + - - - - ## GET diff_summary(id,was_index,now_index) The same as `diff_lines` except its Hash entries do *not* include "lines". From 2910666c6400729c760371f69cb338d1bdd9b2b7 Mon Sep 17 00:00:00 2001 From: JonJagger Date: Wed, 20 May 2026 16:24:54 +0100 Subject: [PATCH 4/4] Flush stdout so test dots appear as each test runs rather than all at the end Co-Authored-By: Claude Sonnet 4.6 --- test/client/lib/run.sh | 1 + test/server/lib/run.sh | 1 + 2 files changed, 2 insertions(+) diff --git a/test/client/lib/run.sh b/test/client/lib/run.sh index 8bb6e878..75d5b065 100755 --- a/test/client/lib/run.sh +++ b/test/client/lib/run.sh @@ -10,6 +10,7 @@ readonly TEST_FILES=(${MY_DIR}/../*_test.rb) readonly TEST_ARGS=(${@}) readonly SCRIPT=" +\$stdout.sync = true require '${MY_DIR}/coverage.rb' %w(${TEST_FILES[*]}).shuffle.each{ |file| require file diff --git a/test/server/lib/run.sh b/test/server/lib/run.sh index 8bb6e878..75d5b065 100755 --- a/test/server/lib/run.sh +++ b/test/server/lib/run.sh @@ -10,6 +10,7 @@ readonly TEST_FILES=(${MY_DIR}/../*_test.rb) readonly TEST_ARGS=(${@}) readonly SCRIPT=" +\$stdout.sync = true require '${MY_DIR}/coverage.rb' %w(${TEST_FILES[*]}).shuffle.each{ |file| require file