diff --git a/bin/generate b/bin/generate new file mode 100755 index 00000000..dadd22ab --- /dev/null +++ b/bin/generate @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# Generate Zig practice-exercise test files from problem-specifications canonical data. +# Usage: bin/generate [ ...] | --all | --check ... +set -euo pipefail +cd "$(dirname "$0")/.." +exec python3 generators/generate.py "$@" diff --git a/exercises/practice/affine-cipher/.meta/supplements.json b/exercises/practice/affine-cipher/.meta/supplements.json new file mode 100644 index 00000000..dcfc3b3a --- /dev/null +++ b/exercises/practice/affine-cipher/.meta/supplements.json @@ -0,0 +1,16 @@ +{ + "cases": [ + { + "description": "encode boundary characters", + "property": "encode", + "input": { + "phrase": "/09:@AMNZ[`amnz{", + "key": { + "a": 25, + "b": 12 + } + }, + "expected": "09maz nmazn" + } + ] +} diff --git a/exercises/practice/binary-search-tree/.meta/supplements.json b/exercises/practice/binary-search-tree/.meta/supplements.json new file mode 100644 index 00000000..1e5ab317 --- /dev/null +++ b/exercises/practice/binary-search-tree/.meta/supplements.json @@ -0,0 +1,25 @@ +{ + "cases": [ + { + "description": "empty tree has null root", + "property": "data", + "input": { + "treeData": [] + }, + "expected": null + }, + { + "description": "can sort data", + "cases": [ + { + "description": "can sort empty tree", + "property": "sortedData", + "input": { + "treeData": [] + }, + "expected": [] + } + ] + } + ] +} diff --git a/exercises/practice/connect/test_connect.zig b/exercises/practice/connect/test_connect.zig index 25ab147a..a11d7b2e 100644 --- a/exercises/practice/connect/test_connect.zig +++ b/exercises/practice/connect/test_connect.zig @@ -10,18 +10,22 @@ test "an empty board has no winner" { " . . . . .", // " . . . . .", // " . . . . .", // - " . . . . .", + " . . . . .", // }; try testing.expectEqual('.', winner(testing.allocator, &board)); } test "X can win on a 1x1 board" { - const board = [_][]const u8{"X"}; + const board = [_][]const u8{ + "X", // + }; try testing.expectEqual('X', winner(testing.allocator, &board)); } test "O can win on a 1x1 board" { - const board = [_][]const u8{"O"}; + const board = [_][]const u8{ + "O", // + }; try testing.expectEqual('O', winner(testing.allocator, &board)); } @@ -30,7 +34,7 @@ test "only edges does not make a winner" { "O O O X", // " X . . X", // " X . . X", // - " X O O O", + " X O O O", // }; try testing.expectEqual('.', winner(testing.allocator, &board)); } @@ -41,7 +45,7 @@ test "illegal diagonal does not make a winner" { " O X X X", // " O X O .", // " . O X .", // - " X X O O", + " X X O O", // }; try testing.expectEqual('.', winner(testing.allocator, &board)); } @@ -52,7 +56,7 @@ test "nobody wins crossing adjacent angles" { " . X O .", // " O . X O", // " . O . X", // - " . . O .", + " . . O .", // }; try testing.expectEqual('.', winner(testing.allocator, &board)); } @@ -63,7 +67,7 @@ test "X wins crossing from left to right" { " O X X X", // " O X O .", // " X X O X", // - " . O X .", + " . O X .", // }; try testing.expectEqual('X', winner(testing.allocator, &board)); } @@ -73,7 +77,7 @@ test "X wins with left-hand dead end fork" { ". . X .", // " X X . .", // " . X X X", // - " O O O O", + " O O O O", // }; try testing.expectEqual('X', winner(testing.allocator, &board)); } @@ -83,7 +87,7 @@ test "X wins with right-hand dead end fork" { ". . X X", // " X X . .", // " . X X .", // - " O O O O", + " O O O O", // }; try testing.expectEqual('X', winner(testing.allocator, &board)); } @@ -94,7 +98,7 @@ test "O wins crossing from top to bottom" { " O X X X", // " O O O .", // " X X O X", // - " . O X .", + " . O X .", // }; try testing.expectEqual('O', winner(testing.allocator, &board)); } @@ -105,7 +109,7 @@ test "X wins using a convoluted path" { " X . X . X", // " . X . X .", // " . X X . .", // - " O O O O O", + " O O O O O", // }; try testing.expectEqual('X', winner(testing.allocator, &board)); } @@ -120,7 +124,7 @@ test "X wins using a spiral path" { " O X O O O X O X O", // " O X X X X X O X O", // " O O O O O O O X O", // - " X X X X X X X X O", + " X X X X X X X X O", // }; try testing.expectEqual('X', winner(testing.allocator, &board)); } diff --git a/exercises/practice/flower-field/test_flower_field.zig b/exercises/practice/flower-field/test_flower_field.zig index 078511bf..b0197b57 100644 --- a/exercises/practice/flower-field/test_flower_field.zig +++ b/exercises/practice/flower-field/test_flower_field.zig @@ -1,21 +1,21 @@ const std = @import("std"); +const mem = std.mem; const testing = std.testing; -const annotate = @import("flower_field.zig").annotate; - -fn free(slices: [][]u8) void { - for (slices) |slice| { - testing.allocator.free(slice); +const flower_field = @import("flower_field.zig"); +fn annotateTest( + allocator: mem.Allocator, + expected: []const []const u8, + garden: []const []const u8, +) anyerror!void { + const actual = try flower_field.annotate(allocator, garden); + defer { + for (actual) |line| allocator.free(line); + allocator.free(actual); } - testing.allocator.free(slices); -} - -fn annotateTest(allocator: std.mem.Allocator, expected: []const []const u8, garden: []const []const u8) !void { - const actual = try annotate(allocator, garden); - defer free(actual); try testing.expectEqual(expected.len, actual.len); - for (0..expected.len) |i| { - try testing.expectEqualStrings(expected[i], actual[i]); + for (expected, actual) |e, a| { + try testing.expectEqualStrings(e, a); } } diff --git a/exercises/practice/high-scores/.meta/supplements.json b/exercises/practice/high-scores/.meta/supplements.json new file mode 100644 index 00000000..89534015 --- /dev/null +++ b/exercises/practice/high-scores/.meta/supplements.json @@ -0,0 +1,19 @@ +{ + "cases": [ + { + "description": "Latest score can be outside top three", + "property": "latest", + "input": { + "scores": [ + 80, + 70, + 90, + 10, + 20, + 30 + ] + }, + "expected": 30 + } + ] +} diff --git a/exercises/practice/matrix/.meta/supplements.json b/exercises/practice/matrix/.meta/supplements.json new file mode 100644 index 00000000..99fcde3b --- /dev/null +++ b/exercises/practice/matrix/.meta/supplements.json @@ -0,0 +1,30 @@ +{ + "cases": [ + { + "description": "row with negative numbers", + "property": "row", + "input": { + "string": "1 2 4\n-57 9 -42\n10 0 65", + "index": 2 + }, + "expected": [ + -57, + 9, + -42 + ] + }, + { + "description": "column with negative numbers", + "property": "column", + "input": { + "string": "1 2 -4\n-57 9 -42\n10 0 -465", + "index": 3 + }, + "expected": [ + -4, + -42, + -465 + ] + } + ] +} diff --git a/exercises/practice/meetup/.meta/supplements.json b/exercises/practice/meetup/.meta/supplements.json new file mode 100644 index 00000000..cb711f47 --- /dev/null +++ b/exercises/practice/meetup/.meta/supplements.json @@ -0,0 +1,26 @@ +{ + "cases": [ + { + "description": "when last Thursday in February in a non-leap year is not the 29th", + "property": "meetup", + "input": { + "year": 2300, + "month": 2, + "week": "last", + "dayofweek": "Thursday" + }, + "expected": "2300-02-22" + }, + { + "description": "when fourth Monday is the 23nd, the second day of the fourth week", + "property": "meetup", + "input": { + "year": 2468, + "month": 1, + "week": "fourth", + "dayofweek": "Monday" + }, + "expected": "2468-01-23" + } + ] +} diff --git a/exercises/practice/micro-blog/.meta/supplements.json b/exercises/practice/micro-blog/.meta/supplements.json new file mode 100644 index 00000000..7db809e1 --- /dev/null +++ b/exercises/practice/micro-blog/.meta/supplements.json @@ -0,0 +1,12 @@ +{ + "cases": [ + { + "description": "ideograms", + "property": "truncate", + "input": { + "phrase": "二兎を追う者は一兎をも得ず" + }, + "expected": "二兎を追う" + } + ] +} diff --git a/exercises/practice/nth-prime/.meta/supplements.json b/exercises/practice/nth-prime/.meta/supplements.json new file mode 100644 index 00000000..05c36c22 --- /dev/null +++ b/exercises/practice/nth-prime/.meta/supplements.json @@ -0,0 +1,28 @@ +{ + "cases": [ + { + "description": "third prime", + "property": "prime", + "input": { "number": 3 }, + "expected": 5 + }, + { + "description": "fourth prime", + "property": "prime", + "input": { "number": 4 }, + "expected": 7 + }, + { + "description": "fifth prime", + "property": "prime", + "input": { "number": 5 }, + "expected": 11 + }, + { + "description": "seventh prime", + "property": "prime", + "input": { "number": 7 }, + "expected": 17 + } + ] +} diff --git a/exercises/practice/nth-prime/test_nth_prime.zig b/exercises/practice/nth-prime/test_nth_prime.zig index a50496e9..90c6d2bf 100644 --- a/exercises/practice/nth-prime/test_nth_prime.zig +++ b/exercises/practice/nth-prime/test_nth_prime.zig @@ -39,6 +39,6 @@ test "seventh prime" { } test "big prime" { - const p = try nth_prime.prime(testing.allocator, 10001); - try testing.expectEqual(104743, p); + const p = try nth_prime.prime(testing.allocator, 10_001); + try testing.expectEqual(104_743, p); } diff --git a/exercises/practice/palindrome-products/.meta/supplements.json b/exercises/practice/palindrome-products/.meta/supplements.json new file mode 100644 index 00000000..201ae563 --- /dev/null +++ b/exercises/practice/palindrome-products/.meta/supplements.json @@ -0,0 +1,72 @@ +{ + "cases": [ + { + "description": "smallest with large factors", + "property": "smallest", + "input": { + "min": 54773, + "max": 63245 + }, + "expected": { + "value": 3030220303, + "factors": [ + [ + 54799, + 55297 + ] + ] + } + }, + { + "description": "largest with large factors", + "property": "largest", + "input": { + "min": 54773, + "max": 63245 + }, + "expected": { + "value": 3956776593, + "factors": [ + [ + 62799, + 63007 + ] + ] + } + }, + { + "description": "smallest with very large factors", + "property": "smallest", + "input": { + "min": 3000100, + "max": 3141592 + }, + "expected": { + "value": 9003210123009, + "factors": [ + [ + 3000131, + 3000939 + ] + ] + } + }, + { + "description": "largest with very large factors", + "property": "largest", + "input": { + "min": 3100000, + "max": 3141592 + }, + "expected": { + "value": 9864278724689, + "factors": [ + [ + 3140089, + 3141401 + ] + ] + } + } + ] +} diff --git a/exercises/practice/pascals-triangle/.meta/supplements.json b/exercises/practice/pascals-triangle/.meta/supplements.json new file mode 100644 index 00000000..469dc874 --- /dev/null +++ b/exercises/practice/pascals-triangle/.meta/supplements.json @@ -0,0 +1,10 @@ +{ + "cases": [ + { + "description": "seventy-five rows", + "property": "rows", + "input": { "count": 75 }, + "expected_element": { "row": 74, "column": 37, "value": 1746130564335626209832 } + } + ] +} diff --git a/exercises/practice/pascals-triangle/test_pascals_triangle.zig b/exercises/practice/pascals-triangle/test_pascals_triangle.zig index 56e79bce..bcc6cded 100644 --- a/exercises/practice/pascals-triangle/test_pascals_triangle.zig +++ b/exercises/practice/pascals-triangle/test_pascals_triangle.zig @@ -2,7 +2,6 @@ const std = @import("std"); const testing = std.testing; const pascals_triangle = @import("pascals_triangle.zig"); - fn free(slices: [][]u128) void { for (slices) |slice| { testing.allocator.free(slice); @@ -10,7 +9,7 @@ fn free(slices: [][]u128) void { testing.allocator.free(slices); } -fn rowsTest(allocator: std.mem.Allocator, count: usize, expected: [][]const u128) anyerror!void { +fn pascalsTriangleTest(allocator: std.mem.Allocator, count: usize, expected: [][]const u128) anyerror!void { const actual = try pascals_triangle.rows(allocator, count); defer free(actual); @@ -24,7 +23,7 @@ test "zero rows" { const expected: [0][]const u128 = undefined; try std.testing.checkAllAllocationFailures( std.testing.allocator, - rowsTest, + pascalsTriangleTest, .{ 0, &expected }, ); } @@ -34,7 +33,7 @@ test "single row" { expected[0] = &.{1}; try std.testing.checkAllAllocationFailures( std.testing.allocator, - rowsTest, + pascalsTriangleTest, .{ 1, &expected }, ); } @@ -45,7 +44,7 @@ test "two rows" { expected[1] = &.{ 1, 1 }; try std.testing.checkAllAllocationFailures( std.testing.allocator, - rowsTest, + pascalsTriangleTest, .{ 2, &expected }, ); } @@ -57,7 +56,7 @@ test "three rows" { expected[2] = &.{ 1, 2, 1 }; try std.testing.checkAllAllocationFailures( std.testing.allocator, - rowsTest, + pascalsTriangleTest, .{ 3, &expected }, ); } @@ -70,7 +69,7 @@ test "four rows" { expected[3] = &.{ 1, 3, 3, 1 }; try std.testing.checkAllAllocationFailures( std.testing.allocator, - rowsTest, + pascalsTriangleTest, .{ 4, &expected }, ); } @@ -84,7 +83,7 @@ test "five rows" { expected[4] = &.{ 1, 4, 6, 4, 1 }; try std.testing.checkAllAllocationFailures( std.testing.allocator, - rowsTest, + pascalsTriangleTest, .{ 5, &expected }, ); } @@ -99,7 +98,7 @@ test "six rows" { expected[5] = &.{ 1, 5, 10, 10, 5, 1 }; try std.testing.checkAllAllocationFailures( std.testing.allocator, - rowsTest, + pascalsTriangleTest, .{ 6, &expected }, ); } @@ -118,7 +117,7 @@ test "ten rows" { expected[9] = &.{ 1, 9, 36, 84, 126, 126, 84, 36, 9, 1 }; try std.testing.checkAllAllocationFailures( std.testing.allocator, - rowsTest, + pascalsTriangleTest, .{ 10, &expected }, ); } @@ -127,5 +126,5 @@ test "seventy-five rows" { const actual = try pascals_triangle.rows(testing.allocator, 75); defer free(actual); - try testing.expectEqual(17_46_130_564_335_626_209_832, actual[74][37]); + try testing.expectEqual(1_746_130_564_335_626_209_832, actual[74][37]); } diff --git a/exercises/practice/piecing-it-together/.meta/supplements.json b/exercises/practice/piecing-it-together/.meta/supplements.json new file mode 100644 index 00000000..9de7a34c --- /dev/null +++ b/exercises/practice/piecing-it-together/.meta/supplements.json @@ -0,0 +1,22 @@ +{ + "cases": [ + { + "description": "very large landscape", + "property": "jigsawData", + "input": { + "border": 1216, + "inside": 86625, + "format": "landscape" + }, + "expected": { + "pieces": 87841, + "border": 1216, + "inside": 86625, + "rows": 233, + "columns": 377, + "aspectRatio": 1.6180257510729614, + "format": "landscape" + } + } + ] +} diff --git a/exercises/practice/pig-latin/test_pig_latin.zig b/exercises/practice/pig-latin/test_pig_latin.zig index 3b39d040..4c2a3b48 100644 --- a/exercises/practice/pig-latin/test_pig_latin.zig +++ b/exercises/practice/pig-latin/test_pig_latin.zig @@ -73,7 +73,7 @@ test "first letter and ay are moved to the end of words that start with consonan try testing.expectEqualStrings(expected, actual); } -test "first letter and ay are moved to the end of words that start with consonants -> word beginning with consonant and vowel containing qu" { +test "first letter and ay are moved to the end of words that start with consonants-word beginning with consonant and vowel containing qu" { const expected: []const u8 = "iquidlay"; const actual = try pig_latin.translate(testing.allocator, "liquid"); defer testing.allocator.free(actual); diff --git a/exercises/practice/prime-factors/.meta/supplements.json b/exercises/practice/prime-factors/.meta/supplements.json new file mode 100644 index 00000000..d46f4c43 --- /dev/null +++ b/exercises/practice/prime-factors/.meta/supplements.json @@ -0,0 +1,16 @@ +{ + "cases": [ + { + "description": "product of three large primes", + "property": "factors", + "input": { "value": 9164464719174396253 }, + "expected": [2077681, 2099191, 2101243] + }, + { + "description": "one very large prime", + "property": "factors", + "input": { "value": 4016465016163 }, + "expected": [4016465016163] + } + ] +} diff --git a/exercises/practice/rail-fence-cipher/.meta/supplements.json b/exercises/practice/rail-fence-cipher/.meta/supplements.json new file mode 100644 index 00000000..016d0ae4 --- /dev/null +++ b/exercises/practice/rail-fence-cipher/.meta/supplements.json @@ -0,0 +1,22 @@ +{ + "cases": [ + { + "description": "encode alphabet", + "property": "encode", + "input": { + "msg": "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + "rails": 2 + }, + "expected": "ACEGIKMOQSUWYBDFHJLNPRTVXZ" + }, + { + "description": "decode alphabet", + "property": "decode", + "input": { + "msg": "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + "rails": 2 + }, + "expected": "ANBOCPDQERFSGTHUIVJWKXLYMZ" + } + ] +} diff --git a/exercises/practice/rail-fence-cipher/test_rail_fence_cipher.zig b/exercises/practice/rail-fence-cipher/test_rail_fence_cipher.zig index 4f2baaef..0749f533 100644 --- a/exercises/practice/rail-fence-cipher/test_rail_fence_cipher.zig +++ b/exercises/practice/rail-fence-cipher/test_rail_fence_cipher.zig @@ -3,6 +3,7 @@ const mem = std.mem; const testing = std.testing; const rail_fence_cipher = @import("rail_fence_cipher.zig"); + const encode = rail_fence_cipher.encode; const decode = rail_fence_cipher.decode; @@ -73,3 +74,23 @@ test "decode with six rails" { .{ &decode, phrase, 6, expect }, ); } + +test "encode alphabet" { + const phrase: []const u8 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + const expect: []const u8 = "ACEGIKMOQSUWYBDFHJLNPRTVXZ"; + try std.testing.checkAllAllocationFailures( + std.testing.allocator, + railFenceCipherTest, + .{ &encode, phrase, 2, expect }, + ); +} + +test "decode alphabet" { + const phrase: []const u8 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + const expect: []const u8 = "ANBOCPDQERFSGTHUIVJWKXLYMZ"; + try std.testing.checkAllAllocationFailures( + std.testing.allocator, + railFenceCipherTest, + .{ &decode, phrase, 2, expect }, + ); +} diff --git a/exercises/practice/rectangles/.meta/supplements.json b/exercises/practice/rectangles/.meta/supplements.json new file mode 100644 index 00000000..d9212007 --- /dev/null +++ b/exercises/practice/rectangles/.meta/supplements.json @@ -0,0 +1,34 @@ +{ + "cases": [ + { + "description": "very large input", + "property": "rectangles", + "input": { + "strings": [ + " +-----+--------+ +-----+ ", + "++---++-----+--------+---++-----++", + "||+--++-----+-+-++ | || ||", + "||| || +-+-++-+ | || ||", + "||| || | | || | | || ||", + "||| +++-----+-+-++-+-+---++-+ ||", + "||| ||| | | || | |+--++-+-+ ||", + "||| +++---+-+-+-++-+-++--++-+ | ||", + "||| |||+--+-+-+-+| | |+--++---+ ||", + "||| |||| | | | || | |+-+|| ||", + "||+-++++--+-+++-++-+-++-+++---++||", + "|| |+++--+-+++-+--+-+| ||| ||||", + "+++-+++++---++--+-++-++-+++---+|||", + " |+-+++++---++--+ || || ||| ||||", + " | +++++---++--+-++-++-++++ ||||", + " | |||| |+----++-++-++++--+++|", + " | |+++---+| || || || || |", + "+++ |||+---++----+| || || || |", + "||| +++----++----++-++-++----++-+", + "+++---++----++-----+-++-++----++ ", + " +-+ " + ] + }, + "expected": 2063 + } + ] +} diff --git a/exercises/practice/rectangles/test_rectangles.zig b/exercises/practice/rectangles/test_rectangles.zig index 4fe574bd..fbc87bee 100644 --- a/exercises/practice/rectangles/test_rectangles.zig +++ b/exercises/practice/rectangles/test_rectangles.zig @@ -177,5 +177,5 @@ test "very large input" { " +-+ ", // }; const count = rectangles.rectangles(&strings); - try testing.expectEqual(2063, count); + try testing.expectEqual(2_063, count); } diff --git a/exercises/practice/resistor-color-trio/.meta/supplements.json b/exercises/practice/resistor-color-trio/.meta/supplements.json new file mode 100644 index 00000000..93285c7d --- /dev/null +++ b/exercises/practice/resistor-color-trio/.meta/supplements.json @@ -0,0 +1,64 @@ +{ + "cases": [ + { + "description": "Orange and orange and red", + "property": "label", + "input": { + "colors": [ + "orange", + "orange", + "red" + ] + }, + "expected": { + "value": "3.3", + "unit": "kiloohms" + } + }, + { + "description": "Orange and orange and green", + "property": "label", + "input": { + "colors": [ + "orange", + "orange", + "green" + ] + }, + "expected": { + "value": "3.3", + "unit": "megaohms" + } + }, + { + "description": "White and white and violet", + "property": "label", + "input": { + "colors": [ + "white", + "white", + "violet" + ] + }, + "expected": { + "value": "990", + "unit": "megaohms" + } + }, + { + "description": "White and white and grey", + "property": "label", + "input": { + "colors": [ + "white", + "white", + "grey" + ] + }, + "expected": { + "value": "9.9", + "unit": "gigaohms" + } + } + ] +} diff --git a/exercises/practice/reverse-string/test_reverse_string.zig b/exercises/practice/reverse-string/test_reverse_string.zig index 01df0306..0c84f771 100644 --- a/exercises/practice/reverse-string/test_reverse_string.zig +++ b/exercises/practice/reverse-string/test_reverse_string.zig @@ -2,9 +2,9 @@ const std = @import("std"); const testing = std.testing; const reverse_string = @import("reverse_string.zig"); +const buffer_size = 80; fn testReverse(comptime s: []const u8, expected: []const u8) !void { - const buffer_size = 80; // exceeds length of test strings var buffer: [buffer_size]u8 = undefined; const actual = reverse_string.reverse(&buffer, s); try testing.expectEqualStrings(expected, actual); diff --git a/exercises/practice/say/.meta/supplements.json b/exercises/practice/say/.meta/supplements.json new file mode 100644 index 00000000..c2d219d1 --- /dev/null +++ b/exercises/practice/say/.meta/supplements.json @@ -0,0 +1,36 @@ +{ + "cases": [ + { + "description": "additional big number", + "property": "say", + "input": { + "number": 19011016013 + }, + "expected": "nineteen billion eleven million sixteen thousand thirteen" + }, + { + "description": "different big number", + "property": "say", + "input": { + "number": 812000070017 + }, + "expected": "eight hundred twelve billion seventy thousand seventeen" + }, + { + "description": "alternative big number", + "property": "say", + "input": { + "number": 60010015018 + }, + "expected": "sixty billion ten million fifteen thousand eighteen" + }, + { + "description": "twelve sevens", + "property": "say", + "input": { + "number": 777777777777 + }, + "expected": "seven hundred seventy-seven billion seven hundred seventy-seven million seven hundred seventy-seven thousand seven hundred seventy-seven" + } + ] +} diff --git a/exercises/practice/series/test_series.zig b/exercises/practice/series/test_series.zig index 63153d0d..0a350c89 100644 --- a/exercises/practice/series/test_series.zig +++ b/exercises/practice/series/test_series.zig @@ -10,10 +10,7 @@ test "slices of one from one" { }; const actual = try slices(1, testing.allocator, series); defer testing.allocator.free(actual); - try testing.expectEqual(expected.len, actual.len); - for (expected, 0..) |expected_slice, i| { - try testing.expectEqualStrings(&expected_slice, &actual[i]); - } + try testing.expectEqualSlices([1]u8, &expected, actual); } test "slices of one from two" { @@ -24,10 +21,7 @@ test "slices of one from two" { }; const actual = try slices(1, testing.allocator, series); defer testing.allocator.free(actual); - try testing.expectEqual(expected.len, actual.len); - for (expected, 0..) |expected_slice, i| { - try testing.expectEqualStrings(&expected_slice, &actual[i]); - } + try testing.expectEqualSlices([1]u8, &expected, actual); } test "slices of two" { @@ -37,10 +31,7 @@ test "slices of two" { }; const actual = try slices(2, testing.allocator, series); defer testing.allocator.free(actual); - try testing.expectEqual(expected.len, actual.len); - for (expected, 0..) |expected_slice, i| { - try testing.expectEqualStrings(&expected_slice, &actual[i]); - } + try testing.expectEqualSlices([2]u8, &expected, actual); } test "slices of two overlap" { @@ -52,10 +43,7 @@ test "slices of two overlap" { }; const actual = try slices(2, testing.allocator, series); defer testing.allocator.free(actual); - try testing.expectEqual(expected.len, actual.len); - for (expected, 0..) |expected_slice, i| { - try testing.expectEqualStrings(&expected_slice, &actual[i]); - } + try testing.expectEqualSlices([2]u8, &expected, actual); } test "slices can include duplicates" { @@ -68,10 +56,7 @@ test "slices can include duplicates" { }; const actual = try slices(3, testing.allocator, series); defer testing.allocator.free(actual); - try testing.expectEqual(expected.len, actual.len); - for (expected, 0..) |expected_slice, i| { - try testing.expectEqualStrings(&expected_slice, &actual[i]); - } + try testing.expectEqualSlices([3]u8, &expected, actual); } test "slices of a long series" { @@ -88,10 +73,7 @@ test "slices of a long series" { }; const actual = try slices(5, testing.allocator, series); defer testing.allocator.free(actual); - try testing.expectEqual(expected.len, actual.len); - for (expected, 0..) |expected_slice, i| { - try testing.expectEqualStrings(&expected_slice, &actual[i]); - } + try testing.expectEqualSlices([5]u8, &expected, actual); } test "slice length is too large" { @@ -99,10 +81,7 @@ test "slice length is too large" { const expected = [_][6]u8{}; const actual = try slices(6, testing.allocator, series); defer testing.allocator.free(actual); - try testing.expectEqual(expected.len, actual.len); - for (expected, 0..) |expected_slice, i| { - try testing.expectEqualStrings(&expected_slice, &actual[i]); - } + try testing.expectEqualSlices([6]u8, &expected, actual); } test "slice length is way too large" { @@ -110,8 +89,5 @@ test "slice length is way too large" { const expected = [_][42]u8{}; const actual = try slices(42, testing.allocator, series); defer testing.allocator.free(actual); - try testing.expectEqual(expected.len, actual.len); - for (expected, 0..) |expected_slice, i| { - try testing.expectEqualStrings(&expected_slice, &actual[i]); - } + try testing.expectEqualSlices([42]u8, &expected, actual); } diff --git a/exercises/practice/state-of-tic-tac-toe/test_state_of_tic_tac_toe.zig b/exercises/practice/state-of-tic-tac-toe/test_state_of_tic_tac_toe.zig index 5fdacd04..5490cc3f 100644 --- a/exercises/practice/state-of-tic-tac-toe/test_state_of_tic_tac_toe.zig +++ b/exercises/practice/state-of-tic-tac-toe/test_state_of_tic_tac_toe.zig @@ -2,6 +2,7 @@ const std = @import("std"); const testing = std.testing; const state_of_tic_tac_toe = @import("state_of_tic_tac_toe.zig"); + const GameState = state_of_tic_tac_toe.GameState; test "Won games-Finished game where X won via left column victory" { diff --git a/exercises/practice/variable-length-quantity/test_variable_length_quantity.zig b/exercises/practice/variable-length-quantity/test_variable_length_quantity.zig index 0c4c678e..4bb675b8 100644 --- a/exercises/practice/variable-length-quantity/test_variable_length_quantity.zig +++ b/exercises/practice/variable-length-quantity/test_variable_length_quantity.zig @@ -48,7 +48,7 @@ test "encode - smallest double byte" { test "encode - arbitrary double byte" { const expected = [_]u8{ 192, 0 }; - const integers = [_]u32{8192}; + const integers = [_]u32{8_192}; const actual = try encode(testing.allocator, &integers); defer testing.allocator.free(actual); try testing.expectEqualSlices(u8, &expected, actual); @@ -64,7 +64,7 @@ test "encode - asymmetric double byte" { test "encode - largest double byte" { const expected = [_]u8{ 255, 127 }; - const integers = [_]u32{16383}; + const integers = [_]u32{16_383}; const actual = try encode(testing.allocator, &integers); defer testing.allocator.free(actual); try testing.expectEqualSlices(u8, &expected, actual); @@ -72,7 +72,7 @@ test "encode - largest double byte" { test "encode - smallest triple byte" { const expected = [_]u8{ 129, 128, 0 }; - const integers = [_]u32{16384}; + const integers = [_]u32{16_384}; const actual = try encode(testing.allocator, &integers); defer testing.allocator.free(actual); try testing.expectEqualSlices(u8, &expected, actual); @@ -80,7 +80,7 @@ test "encode - smallest triple byte" { test "encode - arbitrary triple byte" { const expected = [_]u8{ 192, 128, 0 }; - const integers = [_]u32{1048576}; + const integers = [_]u32{1_048_576}; const actual = try encode(testing.allocator, &integers); defer testing.allocator.free(actual); try testing.expectEqualSlices(u8, &expected, actual); @@ -88,7 +88,7 @@ test "encode - arbitrary triple byte" { test "encode - asymmetric triple byte" { const expected = [_]u8{ 135, 171, 28 }; - const integers = [_]u32{120220}; + const integers = [_]u32{120_220}; const actual = try encode(testing.allocator, &integers); defer testing.allocator.free(actual); try testing.expectEqualSlices(u8, &expected, actual); @@ -96,7 +96,7 @@ test "encode - asymmetric triple byte" { test "encode - largest triple byte" { const expected = [_]u8{ 255, 255, 127 }; - const integers = [_]u32{2097151}; + const integers = [_]u32{2_097_151}; const actual = try encode(testing.allocator, &integers); defer testing.allocator.free(actual); try testing.expectEqualSlices(u8, &expected, actual); @@ -104,7 +104,7 @@ test "encode - largest triple byte" { test "encode - smallest quadruple byte" { const expected = [_]u8{ 129, 128, 128, 0 }; - const integers = [_]u32{2097152}; + const integers = [_]u32{2_097_152}; const actual = try encode(testing.allocator, &integers); defer testing.allocator.free(actual); try testing.expectEqualSlices(u8, &expected, actual); @@ -112,7 +112,7 @@ test "encode - smallest quadruple byte" { test "encode - arbitrary quadruple byte" { const expected = [_]u8{ 192, 128, 128, 0 }; - const integers = [_]u32{134217728}; + const integers = [_]u32{134_217_728}; const actual = try encode(testing.allocator, &integers); defer testing.allocator.free(actual); try testing.expectEqualSlices(u8, &expected, actual); @@ -120,7 +120,7 @@ test "encode - arbitrary quadruple byte" { test "encode - asymmetric quadruple byte" { const expected = [_]u8{ 129, 213, 238, 4 }; - const integers = [_]u32{3503876}; + const integers = [_]u32{3_503_876}; const actual = try encode(testing.allocator, &integers); defer testing.allocator.free(actual); try testing.expectEqualSlices(u8, &expected, actual); @@ -128,7 +128,7 @@ test "encode - asymmetric quadruple byte" { test "encode - largest quadruple byte" { const expected = [_]u8{ 255, 255, 255, 127 }; - const integers = [_]u32{268435455}; + const integers = [_]u32{268_435_455}; const actual = try encode(testing.allocator, &integers); defer testing.allocator.free(actual); try testing.expectEqualSlices(u8, &expected, actual); @@ -136,7 +136,7 @@ test "encode - largest quadruple byte" { test "encode - smallest quintuple byte" { const expected = [_]u8{ 129, 128, 128, 128, 0 }; - const integers = [_]u32{268435456}; + const integers = [_]u32{268_435_456}; const actual = try encode(testing.allocator, &integers); defer testing.allocator.free(actual); try testing.expectEqualSlices(u8, &expected, actual); @@ -144,7 +144,7 @@ test "encode - smallest quintuple byte" { test "encode - arbitrary quintuple byte" { const expected = [_]u8{ 143, 248, 128, 128, 0 }; - const integers = [_]u32{4278190080}; + const integers = [_]u32{4_278_190_080}; const actual = try encode(testing.allocator, &integers); defer testing.allocator.free(actual); try testing.expectEqualSlices(u8, &expected, actual); @@ -152,7 +152,7 @@ test "encode - arbitrary quintuple byte" { test "encode - asymmetric quintuple byte" { const expected = [_]u8{ 136, 179, 149, 194, 5 }; - const integers = [_]u32{2254790917}; + const integers = [_]u32{2_254_790_917}; const actual = try encode(testing.allocator, &integers); defer testing.allocator.free(actual); try testing.expectEqualSlices(u8, &expected, actual); @@ -160,7 +160,7 @@ test "encode - asymmetric quintuple byte" { test "encode - maximum 32-bit integer input" { const expected = [_]u8{ 143, 255, 255, 255, 127 }; - const integers = [_]u32{4294967295}; + const integers = [_]u32{4_294_967_295}; const actual = try encode(testing.allocator, &integers); defer testing.allocator.free(actual); try testing.expectEqualSlices(u8, &expected, actual); @@ -176,7 +176,7 @@ test "encode - two single-byte values" { test "encode - two multi-byte values" { const expected = [_]u8{ 129, 128, 0, 200, 232, 86 }; - const integers = [_]u32{ 16384, 1193046 }; + const integers = [_]u32{ 16_384, 1_193_046 }; const actual = try encode(testing.allocator, &integers); defer testing.allocator.free(actual); try testing.expectEqualSlices(u8, &expected, actual); @@ -184,7 +184,7 @@ test "encode - two multi-byte values" { test "encode - many multi-byte values" { const expected = [_]u8{ 192, 0, 200, 232, 86, 255, 255, 255, 127, 0, 255, 127, 129, 128, 0 }; - const integers = [_]u32{ 8192, 1193046, 268435455, 0, 16383, 16384 }; + const integers = [_]u32{ 8_192, 1_193_046, 268_435_455, 0, 16_383, 16_384 }; const actual = try encode(testing.allocator, &integers); defer testing.allocator.free(actual); try testing.expectEqualSlices(u8, &expected, actual); @@ -199,7 +199,7 @@ test "decode - one byte" { } test "decode - two bytes" { - const expected = [_]u32{8192}; + const expected = [_]u32{8_192}; const integers = [_]u8{ 192, 0 }; const actual = try decode(testing.allocator, &integers); defer testing.allocator.free(actual); @@ -207,7 +207,7 @@ test "decode - two bytes" { } test "decode - three bytes" { - const expected = [_]u32{2097151}; + const expected = [_]u32{2_097_151}; const integers = [_]u8{ 255, 255, 127 }; const actual = try decode(testing.allocator, &integers); defer testing.allocator.free(actual); @@ -215,7 +215,7 @@ test "decode - three bytes" { } test "decode - four bytes" { - const expected = [_]u32{2097152}; + const expected = [_]u32{2_097_152}; const integers = [_]u8{ 129, 128, 128, 0 }; const actual = try decode(testing.allocator, &integers); defer testing.allocator.free(actual); @@ -223,7 +223,7 @@ test "decode - four bytes" { } test "decode - maximum 32-bit integer" { - const expected = [_]u32{4294967295}; + const expected = [_]u32{4_294_967_295}; const integers = [_]u8{ 143, 255, 255, 255, 127 }; const actual = try decode(testing.allocator, &integers); defer testing.allocator.free(actual); @@ -243,7 +243,7 @@ test "decode - incomplete sequence causes error, even if value is zero" { } test "decode - multiple values" { - const expected = [_]u32{ 8192, 1193046, 268435455, 0, 16383, 16384 }; + const expected = [_]u32{ 8_192, 1_193_046, 268_435_455, 0, 16_383, 16_384 }; const integers = [_]u8{ 192, 0, 200, 232, 86, 255, 255, 255, 127, 0, 255, 127, 129, 128, 0 }; const actual = try decode(testing.allocator, &integers); defer testing.allocator.free(actual); diff --git a/exercises/practice/wordy/.meta/supplements.json b/exercises/practice/wordy/.meta/supplements.json new file mode 100644 index 00000000..41065c87 --- /dev/null +++ b/exercises/practice/wordy/.meta/supplements.json @@ -0,0 +1,14 @@ +{ + "cases": [ + { + "description": "reject division by zero", + "property": "answer", + "input": { + "question": "What is 76543 divided by 0?" + }, + "expected": { + "error": "division by zero" + } + } + ] +} diff --git a/generators/.gitignore b/generators/.gitignore new file mode 100644 index 00000000..7a60b85e --- /dev/null +++ b/generators/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.pyc diff --git a/generators/README.md b/generators/README.md new file mode 100644 index 00000000..231dd3e8 --- /dev/null +++ b/generators/README.md @@ -0,0 +1,69 @@ +# Zig test generator + +Generates `exercises/practice//test_.zig` files from the shared +[problem-specifications](https://github.com/exercism/problem-specifications) +`canonical-data.json`. + +## Usage + +Run from the repo root: + +```bash +bin/generate [ ...] # generate specific exercises +bin/generate --all # generate every exercise that has a module +bin/generate --check ... # CI: verify committed files are up to date (no write) +``` + +`zig` must be on `PATH` (or set `ZIG=/path/to/zig`); the output is run through `zig fmt`. + +## Architecture + +Three layers, so per-exercise code stays tiny and consistent: + +- **`generate.py`** — the orchestrator. Owns everything generic: locating canonical data + (via `bin/configlet`), flattening the case tree, joining nested descriptions + (`parent-child`), excluding cases (`reimplements`, unicode `scenarios`, and + `tests.toml` `include = false`), appending + `.meta/supplements.json` cases, wrapping each case as `test "" { ... }`, emitting + the import header, and running `zig fmt`. +- **`lib.py`** — shared Zig value formatters: `zstr` (fully-escaped string literal), + `zint` (`_`-grouped int), `zbool`, `zfloat`, `zmultiline`, `zslice`/`zarray`/`zint_slice`, + and `is_error`. Use these instead of hand-rolling escaping or literals. +- **`exercises/.py`** — one module per exercise, supplying only the exercise-specific + parts. + +## Writing an exercise module + +```python +# generators/exercises/.py +from lib import zstr, zint, is_error # whatever you need + +USE_MEM = False # set True to add `const mem = std.mem;` +IMPORT_SELF = True # default adds `const = @import(".zig");` +HEADER = "" # extra lines after imports: const aliases + helper fns + +def describe(case, parent): # OPTIONAL: override description joining + return case["description"] # e.g. exercises that must not join the group name + +def gen_case(case): # REQUIRED + # case = {"property", "description", "input", "expected", "uuid"} + # return the BODY of the test block; zig fmt fixes indentation. + n = case["input"]["number"] + return f" try testing.expectEqual({case['expected']}, foo.bar({n}));\n" +``` + +Conventions: + +- Always format strings with `zstr` and grouped ints with `zint`. +- Put `const X = .X;` aliases and any test helper `fn`s in `HEADER`. +- Map error cases (`is_error(expected)`) to `testing.expectError(, )` using the + error set from the exercise's `example.zig`. + +## Extra (track-specific) tests + +Cases beyond canonical data live in `exercises/practice//.meta/supplements.json` +(factor-style): + +```json +{ "cases": [ { "description": "...", "property": "...", "input": { ... }, "expected": ... } ] } +``` diff --git a/generators/exercises/__init__.py b/generators/exercises/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/generators/exercises/affine_cipher.py b/generators/exercises/affine_cipher.py new file mode 100644 index 00000000..61202c6c --- /dev/null +++ b/generators/exercises/affine_cipher.py @@ -0,0 +1,34 @@ +from lib import zstr, is_error + +HEADER = ( + "const encode = affine_cipher.encode;\n" + "const decode = affine_cipher.decode;\n" + "const AffineCipherError = affine_cipher.AffineCipherError;" +) + + +def describe(case, parent): + # No parent-description joining, for consistency with existing tests. + return case["description"] + + +def gen_case(case): + inp = case["input"] + phrase = zstr(inp["phrase"]) + a = inp["key"]["a"] + b = inp["key"]["b"] + prop = case["property"] + e = case["expected"] + + if is_error(e): + return ( + f" const actual = {prop}(testing.allocator, {phrase}, {a}, {b});\n" + f" try testing.expectError(AffineCipherError.NotCoprime, actual);\n" + ) + + return ( + f" const expected: []const u8 = {zstr(e)};\n" + f" const actual = try {prop}(testing.allocator, {phrase}, {a}, {b});\n" + f" defer testing.allocator.free(actual);\n" + f" try testing.expectEqualStrings(expected, actual);\n" + ) diff --git a/generators/exercises/binary_search_tree.py b/generators/exercises/binary_search_tree.py new file mode 100644 index 00000000..69a70b4f --- /dev/null +++ b/generators/exercises/binary_search_tree.py @@ -0,0 +1,58 @@ +HEADER = """const Tree = binary_search_tree.Tree; + +fn sortedDataTest(allocator: std.mem.Allocator, tree_data: []const i32, expected: []const i32) anyerror!void { + var tree = Tree.init(allocator); + defer tree.deinit(); + for (tree_data) |data| { + try tree.insert(data); + } + + const actual = try tree.sortedData(allocator); + defer allocator.free(actual); + try testing.expectEqualSlices(i32, expected, actual); +} +""" + + +def _traverse(expr, name, expectation, out): + if expectation is None: + out.append(f"try testing.expectEqual(null, {expr});\n") + return + + if len(name) > 1 and name[0] == "o": + name = name[1:] + + out.append(f"if ({expr}) |{name}| {{\n") + out.append(f"try testing.expectEqual({expectation['data']}, {name}.data);\n") + _traverse(name + ".left", name + "l", expectation["left"], out) + _traverse(name + ".right", name + "r", expectation["right"], out) + out.append("} else {\n") + out.append(f"try testing.expectEqual(false, true); // {expr} should not be null\n") + out.append("}\n") + + +def gen_case(case): + tree_data = case["input"]["treeData"] + expected = case["expected"] + + if case["property"] == "data": + out = [ + "var tree = Tree.init(testing.allocator);\n", + "defer tree.deinit();\n", + ] + for element in tree_data: + out.append(f"try tree.insert({element});\n") + _traverse("tree.root", "o", expected, out) + return "".join(out) + + td = "{ " + ", ".join(tree_data) + " }" + exp = "{ " + ", ".join(expected) + " }" + return ( + f" const tree_data = [_]i32{td};\n" + f" const expected = [_]i32{exp};\n" + " try std.testing.checkAllAllocationFailures(\n" + " std.testing.allocator,\n" + " sortedDataTest,\n" + " .{ &tree_data, &expected },\n" + " );\n" + ) diff --git a/generators/exercises/book_store.py b/generators/exercises/book_store.py new file mode 100644 index 00000000..1e907ee3 --- /dev/null +++ b/generators/exercises/book_store.py @@ -0,0 +1,10 @@ +from lib import zslice + + +def gen_case(case): + basket = case["input"]["basket"] + expected = case["expected"] + return ( + f" const basket = {zslice(basket, 'u32')};\n" + f" try testing.expectEqual({expected}, book_store.total(basket));\n" + ) diff --git a/generators/exercises/bottle_song.py b/generators/exercises/bottle_song.py new file mode 100644 index 00000000..c1b51acc --- /dev/null +++ b/generators/exercises/bottle_song.py @@ -0,0 +1,16 @@ +from lib import zmultiline + + +def gen_case(case): + inp = case["input"] + start, take = inp["startBottles"], inp["takeDown"] + expected = zmultiline("\n".join(case["expected"])) + return ( + " const buffer_size = 4000;\n" + " var buffer: [buffer_size]u8 = undefined;\n" + " const expected: []const u8 =\n" + f"{expected}\n" + " ;\n" + f" const actual = try bottle_song.recite(&buffer, {start}, {take});\n" + " try testing.expectEqualStrings(expected, actual);\n" + ) diff --git a/generators/exercises/change.py b/generators/exercises/change.py new file mode 100644 index 00000000..45a63a55 --- /dev/null +++ b/generators/exercises/change.py @@ -0,0 +1,29 @@ +from lib import zint, zarray, is_error + +# Canonical error message -> the example solution's ChangeError variant. +_ERROR_MAP = { + "can't make target with given coins": "UnreachableTarget", + "target can't be negative": "NegativeTarget", +} + + +def gen_case(case): + coins = case["input"]["coins"] + target = case["input"]["target"] + expected = case["expected"] + coins_lit = zarray([zint(c) for c in coins], "u64") + if is_error(expected): + variant = _ERROR_MAP[expected["error"]] + return ( + f" const coins = {coins_lit};\n" + f" const actual = change.findFewestCoins(testing.allocator, &coins, {zint(target)});\n" + f" try testing.expectError(change.ChangeError.{variant}, actual);\n" + ) + expected_lit = zarray([zint(v) for v in expected], "u64") + return ( + f" const expected = {expected_lit};\n" + f" const coins = {coins_lit};\n" + f" const actual = try change.findFewestCoins(testing.allocator, &coins, {zint(target)});\n" + f" defer testing.allocator.free(actual);\n" + f" try testing.expectEqualSlices(u64, &expected, actual);\n" + ) diff --git a/generators/exercises/circular_buffer.py b/generators/exercises/circular_buffer.py new file mode 100644 index 00000000..ed509c1a --- /dev/null +++ b/generators/exercises/circular_buffer.py @@ -0,0 +1,29 @@ +HEADER = "const CircularBuffer = circular_buffer.CircularBuffer;\nconst BufferError = circular_buffer.BufferError;\n" + + +def gen_case(case): + capacity = case["input"]["capacity"] + operations = case["input"]["operations"] + + lines = [f" var cb = CircularBuffer(i16, {capacity}).init();\n"] + for op in operations: + kind = op["operation"] + if kind == "write": + if op["should_succeed"]: + lines.append(f" try cb.write({op['item']});\n") + else: + lines.append( + f" try testing.expectError(BufferError.BufferOverflow, cb.write({op['item']}));\n" + ) + elif kind == "read": + if op["should_succeed"]: + lines.append( + f" try testing.expectEqual({op['expected']}, cb.read());\n" + ) + else: + lines.append(" try testing.expectEqual(null, cb.read());\n") + elif kind == "overwrite": + lines.append(f" cb.overwrite({op['item']});\n") + elif kind == "clear": + lines.append(" cb.clear();\n") + return "".join(lines) diff --git a/generators/exercises/connect.py b/generators/exercises/connect.py new file mode 100644 index 00000000..4cccec11 --- /dev/null +++ b/generators/exercises/connect.py @@ -0,0 +1,14 @@ +from lib import zstr, zcomment_list + +HEADER = "const winner = connect.winner;\n" + + +def gen_case(case): + rows = case["input"]["board"] + expected = case["expected"] or "." + # One row per line (trailing `//`) so the board grid stays readable. + board = zcomment_list([zstr(row) for row in rows]) + return ( + f" const board = [_][]const u8{board};\n" + f" try testing.expectEqual('{expected}', winner(testing.allocator, &board));\n" + ) diff --git a/generators/exercises/crypto_square.py b/generators/exercises/crypto_square.py new file mode 100644 index 00000000..99dd1938 --- /dev/null +++ b/generators/exercises/crypto_square.py @@ -0,0 +1,12 @@ +from lib import zstr + + +def gen_case(case): + plaintext = zstr(case["input"]["plaintext"]) + e = zstr(case["expected"]) + return ( + f" const expected: []const u8 = {e};\n" + f" const actual = try crypto_square.ciphertext(testing.allocator, {plaintext});\n" + f" defer testing.allocator.free(actual);\n" + f" try testing.expectEqualStrings(expected, actual);\n" + ) diff --git a/generators/exercises/diamond.py b/generators/exercises/diamond.py new file mode 100644 index 00000000..7e63280b --- /dev/null +++ b/generators/exercises/diamond.py @@ -0,0 +1,36 @@ +from lib import zstr, zcomment_list + +IMPORT_SELF = False + +HEADER = """const rows = @import("diamond.zig").rows; + +fn free(slices: [][]u8) void { + for (slices) |slice| { + testing.allocator.free(slice); + } + testing.allocator.free(slices); +} + +fn testRows(allocator: std.mem.Allocator, expected: []const []const u8, letter: u8) !void { + const actual = try rows(allocator, letter); + defer free(actual); + try testing.expectEqual(expected.len, actual.len); + for (0..expected.len) |i| { + try testing.expectEqualStrings(expected[i], actual[i]); + } +} +""" + + +def gen_case(case): + letter = case["input"]["letter"] + expected = case["expected"] + body = zcomment_list([zstr(row) for row in expected]) + return ( + f" const expected = [_][]const u8{body};\n" + f" try std.testing.checkAllAllocationFailures(\n" + f" std.testing.allocator,\n" + f" testRows,\n" + f" .{{ &expected, '{letter}' }},\n" + f" );\n" + ) diff --git a/generators/exercises/dominoes.py b/generators/exercises/dominoes.py new file mode 100644 index 00000000..fb477300 --- /dev/null +++ b/generators/exercises/dominoes.py @@ -0,0 +1,14 @@ +def gen_case(case): + stones = case["input"]["dominoes"] + expected = case["expected"] + op = "" if expected else "!" + + serialized = ", ".join(f"[2]u3{{ {a}, {b} }}" for a, b in stones) + if serialized: + stones_lit = f"&[_][2]u3{{ {serialized} }}" + else: + stones_lit = "&[_][2]u3{}" + return ( + f" const stones = {stones_lit};\n" + f" try testing.expect({op}try dominoes.canChain(testing.allocator, stones));\n" + ) diff --git a/generators/exercises/eliuds_eggs.py b/generators/exercises/eliuds_eggs.py new file mode 100644 index 00000000..ec557c95 --- /dev/null +++ b/generators/exercises/eliuds_eggs.py @@ -0,0 +1,8 @@ +def gen_case(case): + n = case["input"]["number"] + e = case["expected"] + return ( + f" const expected: usize = {e};\n" + f" const actual = eliuds_eggs.eggCount({n});\n" + f" try testing.expectEqual(expected, actual);\n" + ) diff --git a/generators/exercises/etl.py b/generators/exercises/etl.py new file mode 100644 index 00000000..31024ca7 --- /dev/null +++ b/generators/exercises/etl.py @@ -0,0 +1,22 @@ +from lib import zstr + +HEADER = "const transform = etl.transform;\n" + + +def gen_case(case): + legacy = case["input"]["legacy"] + expected = case["expected"] + + lines = [ + " var legacy = std.AutoHashMap(i5, []const u8).init(testing.allocator);\n" + ] + for score in sorted(legacy): + letters = legacy[score] + lines.append(f" try legacy.put({int(score)}, {zstr(''.join(letters))});\n") + lines.append(" var actual = try transform(testing.allocator, legacy);\n") + lines.append(" legacy.deinit();\n\n") + lines.append(f" try testing.expectEqual({len(expected)}, actual.count());\n") + for letter, score in expected.items(): + lines.append(f" try testing.expectEqual({score}, actual.get('{letter}'));\n") + lines.append(" actual.deinit();\n") + return "".join(lines) diff --git a/generators/exercises/flatten_array.py b/generators/exercises/flatten_array.py new file mode 100644 index 00000000..514633f0 --- /dev/null +++ b/generators/exercises/flatten_array.py @@ -0,0 +1,61 @@ +from lib import zint + +HEADER = """const flatten = flatten_array.flatten; +const Box = flatten_array.Box; + +fn flattenTest( + allocator: std.mem.Allocator, + box: Box, + expected: []const i12, +) !void { + const actual: []i12 = try flatten(allocator, box); + defer allocator.free(actual); + try testing.expectEqualSlices(i12, expected, actual); +} +""" + + +def _render_box(value): + """Render a canonical array element into a Box expression.""" + if value is None: + return ".none" + if isinstance(value, list): + if not value: + return "Box{ .many = &[_]Box{} }" + return "Box{\n.many = " + _render_many(value) + ",\n}" + return f"Box{{ .one = {zint(value)} }}" + + +def _render_many(elements): + """Render a list of elements as &[_]Box{ ... } with trailing // comments.""" + if not elements: + return "&[_]Box{}" + body = "".join(f"\n{_render_box(e)}, //" for e in elements) + return "&[_]Box{" + body + "\n}" + + +def gen_case(case): + array = case["input"]["array"] + expected = case["expected"] + + if not array: + box = "const box: Box = Box{ .many = &[_]Box{} };\n" + else: + box = "const box: Box = Box{\n.many = " + _render_many(array) + ",\n};\n" + + if expected: + exp_inner = ", ".join(zint(v) for v in expected) + exp = f"const expected = [_]i12{{ {exp_inner} }};\n" + else: + exp = "const expected = [_]i12{};\n" + + out = ( + box + + exp + + "try std.testing.checkAllAllocationFailures(\n" + + "std.testing.allocator,\n" + + "flattenTest,\n" + + ".{ box, &expected },\n" + + ");\n" + ) + return out diff --git a/generators/exercises/flower_field.py b/generators/exercises/flower_field.py new file mode 100644 index 00000000..f3ce3ebe --- /dev/null +++ b/generators/exercises/flower_field.py @@ -0,0 +1,40 @@ +from lib import zstr + +USE_MEM = True + +HEADER = """fn annotateTest( + allocator: mem.Allocator, + expected: []const []const u8, + garden: []const []const u8, +) anyerror!void { + const actual = try flower_field.annotate(allocator, garden); + defer { + for (actual) |line| allocator.free(line); + allocator.free(actual); + } + try testing.expectEqual(expected.len, actual.len); + for (expected, actual) |e, a| { + try testing.expectEqualStrings(e, a); + } +}""" + + +def _rows(rows): + if not rows: + return "" + inner = ", //".join("\n " + zstr(r) for r in rows) + return inner + " //\n " + + +def gen_case(case): + garden = case["input"]["garden"] + expected = case["expected"] + return ( + f" const garden = [_][]const u8{{{_rows(garden)}}};\n" + f" const expected = [_][]const u8{{{_rows(expected)}}};\n" + " try std.testing.checkAllAllocationFailures(\n" + " std.testing.allocator,\n" + " annotateTest,\n" + " .{ &expected, &garden },\n" + " );\n" + ) diff --git a/generators/exercises/food_chain.py b/generators/exercises/food_chain.py new file mode 100644 index 00000000..a5e1eb38 --- /dev/null +++ b/generators/exercises/food_chain.py @@ -0,0 +1,16 @@ +from lib import zmultiline + + +def gen_case(case): + inp = case["input"] + start, end = inp["startVerse"], inp["endVerse"] + expected = zmultiline("\n".join(case["expected"])) + return ( + " const buffer_size = 4000;\n" + " var buffer: [buffer_size]u8 = undefined;\n" + " const expected: []const u8 =\n" + f"{expected}\n" + " ;\n" + f" const actual = try food_chain.recite(&buffer, {start}, {end});\n" + " try testing.expectEqualStrings(expected, actual);\n" + ) diff --git a/generators/exercises/high_scores.py b/generators/exercises/high_scores.py new file mode 100644 index 00000000..c406a535 --- /dev/null +++ b/generators/exercises/high_scores.py @@ -0,0 +1,16 @@ +HEADER = "const HighScores = high_scores.HighScores;" + + +def gen_case(case): + scores = ", ".join(map(str, case["input"]["scores"])) + prop = case["property"] + e = case["expected"] + s = f" const scores = &[_]i32{{ {scores} }};\n" + if prop == "latest": + s += f" try testing.expectEqual({e}, HighScores.init(scores).latest());\n" + elif prop == "personalBest": + s += f" try testing.expectEqual({e}, HighScores.init(scores).personalBest());\n" + elif prop == "personalTopThree": + exp = ", ".join(map(str, e)) + s += f" try testing.expectEqualSlices(i32, &[_]i32{{ {exp} }}, HighScores.init(scores).personalTopThree());\n" + return s diff --git a/generators/exercises/house.py b/generators/exercises/house.py new file mode 100644 index 00000000..e776fa64 --- /dev/null +++ b/generators/exercises/house.py @@ -0,0 +1,16 @@ +from lib import zmultiline + + +def gen_case(case): + inp = case["input"] + start, end = inp["startVerse"], inp["endVerse"] + expected = zmultiline("\n".join(case["expected"])) + return ( + " const buffer_size = 4000;\n" + " var buffer: [buffer_size]u8 = undefined;\n" + " const expected: []const u8 =\n" + f"{expected}\n" + " ;\n" + f" const actual = try house.recite(&buffer, {start}, {end});\n" + " try testing.expectEqualStrings(expected, actual);\n" + ) diff --git a/generators/exercises/intergalactic_transmission.py b/generators/exercises/intergalactic_transmission.py new file mode 100644 index 00000000..a389aec5 --- /dev/null +++ b/generators/exercises/intergalactic_transmission.py @@ -0,0 +1,35 @@ +from lib import is_error + +HEADER = ( + "const transmitSequence = intergalactic_transmission.transmitSequence;\n" + "const decodeMessage = intergalactic_transmission.decodeMessage;\n" + "const TransmissionError = intergalactic_transmission.TransmissionError;\n" +) + + +def _bytes_lit(values): + # Keep the canonical hex spelling (e.g. 0x03) so the byte values stay legible. + inner = ", ".join(f"0x{int(v, 16):02x}" for v in values) + return "{ " + inner + " }" if values else "{}" + + +def gen_case(case): + prop = case["property"] + message = case["input"]["message"] + expected = case["expected"] + msg_lit = _bytes_lit(message) + + if is_error(expected): + return ( + f" const message = [_]u8{msg_lit};\n" + f" try testing.expectError(TransmissionError.WrongParity, {prop}(testing.allocator, &message));\n" + ) + + exp_lit = _bytes_lit(expected) + return ( + f" const message = [_]u8{msg_lit};\n" + f" const expected = [_]u8{exp_lit};\n" + f" const actual = try {prop}(testing.allocator, &message);\n" + f" defer testing.allocator.free(actual);\n" + f" try testing.expectEqualSlices(u8, &expected, actual);\n" + ) diff --git a/generators/exercises/isbn_verifier.py b/generators/exercises/isbn_verifier.py new file mode 100644 index 00000000..fd590a27 --- /dev/null +++ b/generators/exercises/isbn_verifier.py @@ -0,0 +1,21 @@ +from lib import zstr + +IMPORT_SELF = False +HEADER = 'const isValidIsbn10 = @import("isbn_verifier.zig").isValidIsbn10;' + + +def describe(case, parent): + # Canonical descriptions spell it "isbn"; the track wants "ISBN". + desc = case["description"] + if parent: + desc = f"{parent}-{desc}" + return desc.replace("isbn", "ISBN") + + +def gen_case(case): + isbn = case["input"]["isbn"] + expected = case["expected"] + call = f"isValidIsbn10({zstr(isbn)})" + if expected: + return f" try testing.expect({call});\n" + return f" try testing.expect(!{call});\n" diff --git a/generators/exercises/killer_sudoku_helper.py b/generators/exercises/killer_sudoku_helper.py new file mode 100644 index 00000000..2bea7caf --- /dev/null +++ b/generators/exercises/killer_sudoku_helper.py @@ -0,0 +1,23 @@ +def _bitset(numbers): + result = 0 + for number in numbers: + result |= 1 << (number - 1) + return result + + +def gen_case(case): + cage = case["input"]["cage"] + sm = cage["sum"] + size = cage["size"] + exclude = bin(_bitset(cage["exclude"])) + + combos = sorted(_bitset(combo) for combo in case["expected"]) + expectation = "{ " + ", ".join(bin(c) for c in combos) + " }" + + return ( + " const buffer_size = 200;\n" + " var buffer: [buffer_size]u9 = undefined;\n" + f" const expected = [_]u9{expectation};\n" + f" const actual = killer_sudoku_helper.combinations(&buffer, {sm}, {size}, {exclude});\n" + " try testing.expectEqualSlices(u9, &expected, actual);\n" + ) diff --git a/generators/exercises/kindergarten_garden.py b/generators/exercises/kindergarten_garden.py new file mode 100644 index 00000000..3cc96919 --- /dev/null +++ b/generators/exercises/kindergarten_garden.py @@ -0,0 +1,15 @@ +from lib import zstr, zmultiline + + +def gen_case(case): + diagram = case["input"]["diagram"] + student = case["input"]["student"] + expected = "{ " + ", ".join("." + p for p in case["expected"]) + " }" + return ( + f" const diagram: []const u8 =\n" + f"{zmultiline(diagram)}\n" + f" ;\n" + f" const expected = .{expected};\n" + f" const actual = kindergarten_garden.plants(diagram, {zstr(student)});\n" + f" try testing.expectEqual(expected, actual);\n" + ) diff --git a/generators/exercises/knapsack.py b/generators/exercises/knapsack.py new file mode 100644 index 00000000..96fdc263 --- /dev/null +++ b/generators/exercises/knapsack.py @@ -0,0 +1,19 @@ +HEADER = "const Item = knapsack.Item;\n" + + +def gen_case(case): + input = case["input"] + items = input["items"] + maximum_weight = input["maximumWeight"] + expected = case["expected"] + + lines = [f" const expected: usize = {expected};\n"] + lines.append(f" const items: [{len(items)}]Item = .{{\n") + for item in items: + lines.append(f" Item.init({item['weight']}, {item['value']}),\n") + lines.append(" };\n") + lines.append( + f" const actual = try knapsack.maximumValue(testing.allocator, {maximum_weight}, &items);\n" + ) + lines.append(" try testing.expectEqual(expected, actual);\n") + return "".join(lines) diff --git a/generators/exercises/largest_series_product.py b/generators/exercises/largest_series_product.py new file mode 100644 index 00000000..09456680 --- /dev/null +++ b/generators/exercises/largest_series_product.py @@ -0,0 +1,26 @@ +from lib import zstr, is_error + + +def gen_case(case): + inp = case["input"] + digits = inp["digits"] + span = inp["span"] + e = case["expected"] + + if is_error(e): + if span < 0: + err = "NegativeSpan" + elif span > len(digits): + err = "InsufficientDigits" + else: + err = "InvalidCharacter" + return ( + f" const actual = largest_series_product.largestProduct({zstr(digits)}, {span});\n" + f" try testing.expectError(largest_series_product.SeriesError.{err}, actual);\n" + ) + + return ( + f" const expected: u64 = {e};\n" + f" const actual = try largest_series_product.largestProduct({zstr(digits)}, {span});\n" + f" try testing.expectEqual(expected, actual);\n" + ) diff --git a/generators/exercises/line_up.py b/generators/exercises/line_up.py new file mode 100644 index 00000000..3fe67c4f --- /dev/null +++ b/generators/exercises/line_up.py @@ -0,0 +1,15 @@ +from lib import zstr + +HEADER = "const format = line_up.format;\n" + + +def gen_case(case): + name = case["input"]["name"] + number = case["input"]["number"] + expected = case["expected"] + return ( + f" const expected: []const u8 = {zstr(expected)};\n" + f" const actual = try format(testing.allocator, {zstr(name)}, {number});\n" + f" defer testing.allocator.free(actual);\n" + f" try testing.expectEqualStrings(expected, actual);\n" + ) diff --git a/generators/exercises/matrix.py b/generators/exercises/matrix.py new file mode 100644 index 00000000..1533baaf --- /dev/null +++ b/generators/exercises/matrix.py @@ -0,0 +1,17 @@ +from lib import zstr + +HEADER = "const row = matrix.row;\nconst column = matrix.column;\n" + + +def gen_case(case): + prop = case["property"] # "row" or "column" + s = case["input"]["string"] + index = case["input"]["index"] + expected = ", ".join(str(v) for v in case["expected"]) + return ( + f" const expected = &[_]i16{{ {expected} }};\n" + f" const s = {zstr(s)};\n" + f" const actual = try {prop}(testing.allocator, s, {index});\n" + f" defer testing.allocator.free(actual);\n" + f" try testing.expectEqualSlices(i16, expected, actual);\n" + ) diff --git a/generators/exercises/meetup.py b/generators/exercises/meetup.py new file mode 100644 index 00000000..4fa1b050 --- /dev/null +++ b/generators/exercises/meetup.py @@ -0,0 +1,36 @@ +from lib import zstr + +HEADER = ( + "const Month = meetup.Month;\n" + "const Week = meetup.Week;\n" + "const DayOfWeek = meetup.DayOfWeek;\n" +) + +_MONTHS = [ + "january", + "february", + "march", + "april", + "may", + "june", + "july", + "august", + "september", + "october", + "november", + "december", +] + + +def gen_case(case): + inp = case["input"] + year = inp["year"] + month = _MONTHS[inp["month"] - 1] + week = inp["week"].lower() + day = inp["dayofweek"].lower() + expected = case["expected"] + return ( + f" const expected = {zstr(expected)};\n" + f" const actual = meetup.meetup({year}, .{month}, .{week}, .{day});\n" + f" try testing.expectEqualStrings(expected, &actual);\n" + ) diff --git a/generators/exercises/micro_blog.py b/generators/exercises/micro_blog.py new file mode 100644 index 00000000..d6d9b23b --- /dev/null +++ b/generators/exercises/micro_blog.py @@ -0,0 +1,11 @@ +from lib import zstr + + +def gen_case(case): + phrase = case["input"]["phrase"] + expected = case["expected"] + return ( + f" const expected: []const u8 = {zstr(expected)};\n" + f" const actual = micro_blog.truncate({zstr(phrase)});\n" + f" try testing.expectEqualStrings(expected, actual);\n" + ) diff --git a/generators/exercises/nth_prime.py b/generators/exercises/nth_prime.py new file mode 100644 index 00000000..7e10ad25 --- /dev/null +++ b/generators/exercises/nth_prime.py @@ -0,0 +1,24 @@ +from lib import zint, is_error + +# The "there is no zeroth prime" error case is excluded via `include = false`. + + +def order_key(case): + # Order by n so the supplemental cases (3rd/4th/5th/7th) interleave with the canonical + # ones (1st/2nd/6th/big) by ascending prime index, matching the original ordering. + return case["input"]["number"] + + +def gen_case(case): + n = case["input"]["number"] + expected = case["expected"] + if is_error(expected): + # Defensive: not reached because the error case is excluded above. + return ( + f" const actual = nth_prime.prime(testing.allocator, {zint(n)});\n" + f" try testing.expectError(error.NoZerothPrime, actual);\n" + ) + return ( + f" const p = try nth_prime.prime(testing.allocator, {zint(n)});\n" + f" try testing.expectEqual({zint(expected)}, p);\n" + ) diff --git a/generators/exercises/ocr_numbers.py b/generators/exercises/ocr_numbers.py new file mode 100644 index 00000000..26111125 --- /dev/null +++ b/generators/exercises/ocr_numbers.py @@ -0,0 +1,36 @@ +from lib import zstr, is_error + +HEADER = """const convert = ocr_numbers.convert; +const RecognitionError = ocr_numbers.RecognitionError; + +const buffer_size = 80; +""" + +_ERRORS = { + "Number of input lines is not a multiple of four": "InvalidRowCount", + "Number of input columns is not a multiple of three": "InvalidColumnCount", +} + + +def gen_case(case): + rows = case["input"]["rows"] + expected = case["expected"] + lines = "".join(f" {zstr(r)}, //\n" for r in rows) + body = ( + " var buffer: [buffer_size]u8 = undefined;\n" + " const input = [_][]const u8{\n" + f"{lines}" + " };\n" + ) + if is_error(expected): + err = _ERRORS[expected["error"]] + body += ( + f" try testing.expectError(RecognitionError.{err}, " + "convert(&buffer, &input));\n" + ) + else: + body += ( + f" try testing.expectEqualStrings({zstr(expected)}, " + "try convert(&buffer, &input));\n" + ) + return body diff --git a/generators/exercises/palindrome_products.py b/generators/exercises/palindrome_products.py new file mode 100644 index 00000000..a9228d6c --- /dev/null +++ b/generators/exercises/palindrome_products.py @@ -0,0 +1,40 @@ +USE_MEM = False + +HEADER = """const Palindrome = palindrome_products.Palindrome; +const smallest = palindrome_products.smallest; +const largest = palindrome_products.largest; +""" + + +def gen_case(case): + prop = case["property"] + min_ = case["input"]["min"] + max_ = case["input"]["max"] + expected = case["expected"] + value = expected["value"] + factors = expected["factors"] + + if value is None: + return ( + f" if (try {prop}(testing.allocator, {min_}, {max_})) |_| {{\n" + " try testing.expect(false);\n" + " }\n" + ) + + out = [ + f" if (try {prop}(testing.allocator, {min_}, {max_})) |actual| {{\n", + " defer testing.allocator.free(actual.factors);\n", + f" try testing.expectEqual({value}, actual.value);\n", + f" try testing.expectEqual({len(factors)}, actual.factors.len);\n", + ] + for i, pair in enumerate(factors): + out.append( + f" try testing.expectEqual({pair[0]}, actual.factors[{i}].first);\n" + ) + out.append( + f" try testing.expectEqual({pair[1]}, actual.factors[{i}].second);\n" + ) + out.append(" } else {\n") + out.append(" try testing.expect(false);\n") + out.append(" }\n") + return "".join(out) diff --git a/generators/exercises/pascals_triangle.py b/generators/exercises/pascals_triangle.py new file mode 100644 index 00000000..21b4587a --- /dev/null +++ b/generators/exercises/pascals_triangle.py @@ -0,0 +1,50 @@ +from lib import zint + +USE_MEM = False + +HEADER = """fn free(slices: [][]u128) void { + for (slices) |slice| { + testing.allocator.free(slice); + } + testing.allocator.free(slices); +} + +fn pascalsTriangleTest(allocator: std.mem.Allocator, count: usize, expected: [][]const u128) anyerror!void { + const actual = try pascals_triangle.rows(allocator, count); + defer free(actual); + + try testing.expectEqual(expected.len, actual.len); + for (expected, actual) |expected_slice, actual_slice| { + try testing.expectEqualSlices(u128, expected_slice, actual_slice); + } +} +""" + + +def gen_case(case): + count = case["input"]["count"] + + # Supplemental single-element check (e.g. the "seventy-five rows" case): rather than + # build a full expected triangle, assert one cell. Driven by an `expected_element` + # key carrying {row, column, value}. + if "expected_element" in case: + el = case["expected_element"] + return ( + f" const actual = try pascals_triangle.rows(testing.allocator, {count});\n" + " defer free(actual);\n\n" + f" try testing.expectEqual({zint(el['value'])}, actual[{el['row']}][{el['column']}]);\n" + ) + + expected = case["expected"] + var = "const" if expected == [] else "var" + + out = [f" {var} expected: [{count}][]const u128 = undefined;\n"] + for i, row in enumerate(expected): + slice = "{ " + ", ".join(str(v) for v in row) + " }" + out.append(f" expected[{i}] = &.{slice};\n") + out.append(" try std.testing.checkAllAllocationFailures(\n") + out.append(" std.testing.allocator,\n") + out.append(" pascalsTriangleTest,\n") + out.append(f" .{{ {count}, &expected }},\n") + out.append(" );\n") + return "".join(out) diff --git a/generators/exercises/perfect_numbers.py b/generators/exercises/perfect_numbers.py new file mode 100644 index 00000000..632e0fcd --- /dev/null +++ b/generators/exercises/perfect_numbers.py @@ -0,0 +1,25 @@ +from lib import zint + +HEADER = ( + "const Classification = perfect_numbers.Classification;\n" + "const classify = perfect_numbers.classify;\n" +) + +# The two error cases ("zero is rejected ...", "negative integer is rejected ...") are +# excluded via `include = false` in tests.toml: the example solution's `classify` asserts +# a nonzero positive input at comptime and has no error variant. + + +def describe(case, parent): + # perfect-numbers never joins the parent description, and lowercases the result. + return case["description"].lower() + + +def gen_case(case): + n = case["input"]["number"] + expected = case["expected"] + return ( + f" const expected = Classification.{expected};\n" + f" const actual = classify({zint(n)});\n" + f" try testing.expectEqual(expected, actual);\n" + ) diff --git a/generators/exercises/phone_number.py b/generators/exercises/phone_number.py new file mode 100644 index 00000000..3a3e8a66 --- /dev/null +++ b/generators/exercises/phone_number.py @@ -0,0 +1,15 @@ +from lib import zstr, is_error + + +def gen_case(case): + phrase = case["input"]["phrase"] + expected = case["expected"] + if is_error(expected): + expected_lit = "null" + else: + expected_lit = f"{zstr(expected)}.*" + return ( + f" const expected: ?[10]u8 = {expected_lit};\n" + f" const actual = phone_number.clean({zstr(phrase)});\n" + f" try testing.expectEqual(expected, actual);\n" + ) diff --git a/generators/exercises/piecing_it_together.py b/generators/exercises/piecing_it_together.py new file mode 100644 index 00000000..7868717b --- /dev/null +++ b/generators/exercises/piecing_it_together.py @@ -0,0 +1,34 @@ +HEADER = """const jigsawData = piecing_it_together.jigsawData; +const PuzzleError = piecing_it_together.PuzzleError; +const Format = piecing_it_together.Format; +const PartialInformation = piecing_it_together.PartialInformation; +const FullInformation = piecing_it_together.FullInformation; +""" + +KEYS = ["pieces", "border", "inside", "rows", "columns", "aspectRatio", "format"] + + +def _struct(puzzle): + result = "{\n" + for key in KEYS: + if key in puzzle: + value = puzzle[key] + if key == "format": + value = "." + value + result += f" .{key} = {value},\n" + result += " }" + return result + + +def gen_case(case): + expected = case["expected"] + out = f" const puzzle = PartialInformation{_struct(case['input'])};\n" + if "error" in expected: + error = expected["error"].replace(" data", "Data") + out += " const actual = jigsawData(puzzle);\n" + out += f" try testing.expectError(PuzzleError.{error}, actual);\n" + else: + out += f" const expected = FullInformation{_struct(expected)};\n" + out += " const actual = try jigsawData(puzzle);\n" + out += " try testing.expectEqual(expected, actual);\n" + return out diff --git a/generators/exercises/pig_latin.py b/generators/exercises/pig_latin.py new file mode 100644 index 00000000..f92066ee --- /dev/null +++ b/generators/exercises/pig_latin.py @@ -0,0 +1,12 @@ +from lib import zstr + + +def gen_case(case): + phrase = case["input"]["phrase"] + expected = case["expected"] + return ( + f" const expected: []const u8 = {zstr(expected)};\n" + f" const actual = try pig_latin.translate(testing.allocator, {zstr(phrase)});\n" + f" defer testing.allocator.free(actual);\n" + f" try testing.expectEqualStrings(expected, actual);\n" + ) diff --git a/generators/exercises/prime_factors.py b/generators/exercises/prime_factors.py new file mode 100644 index 00000000..5fb86e61 --- /dev/null +++ b/generators/exercises/prime_factors.py @@ -0,0 +1,12 @@ +from lib import zarray + + +def gen_case(case): + value = case["input"]["value"] + expected = case["expected"] + return ( + f" const expected = {zarray(expected, 'u64')};\n" + f" const actual = try prime_factors.factors(testing.allocator, {value});\n" + f" defer testing.allocator.free(actual);\n" + f" try testing.expectEqualSlices(u64, &expected, actual);\n" + ) diff --git a/generators/exercises/protein_translation.py b/generators/exercises/protein_translation.py new file mode 100644 index 00000000..1aebffff --- /dev/null +++ b/generators/exercises/protein_translation.py @@ -0,0 +1,24 @@ +from lib import zstr, is_error + +HEADER = """const proteins = protein_translation.proteins; +const Protein = protein_translation.Protein; +const TranslationError = protein_translation.TranslationError; +""" + + +def gen_case(case): + strand = zstr(case["input"]["strand"]) + expected = case["expected"] + if is_error(expected): + return ( + " try testing.expectError(" + f"TranslationError.InvalidCodon, proteins(testing.allocator, {strand}));\n" + ) + tags = ", ".join("." + p.lower() for p in expected) + arr = f"[_]Protein{{{tags}}}" if not tags else f"[_]Protein{{ {tags} }}" + return ( + f" const expected = {arr};\n" + f" const actual = try proteins(testing.allocator, {strand});\n" + " defer testing.allocator.free(actual);\n" + " try testing.expectEqualSlices(Protein, &expected, actual);\n" + ) diff --git a/generators/exercises/pythagorean_triplet.py b/generators/exercises/pythagorean_triplet.py new file mode 100644 index 00000000..3c103e6c --- /dev/null +++ b/generators/exercises/pythagorean_triplet.py @@ -0,0 +1,19 @@ +HEADER = ( + "const Triplet = pythagorean_triplet.Triplet;\n" + "const tripletsWithSum = pythagorean_triplet.tripletsWithSum;\n" +) + + +def gen_case(case): + n = case["input"]["n"] + triplets = case["expected"] + count = len(triplets) + lines = [f" const expected: [{count}]Triplet = .{{"] + for a, b, c in triplets: + lines.append(f" Triplet.init({a}, {b}, {c}),") + lines.append(" };") + body = "\n".join(lines) + "\n" + body += f" const actual = try tripletsWithSum(testing.allocator, {n});\n" + body += " defer testing.allocator.free(actual);\n" + body += " try testing.expectEqualSlices(Triplet, &expected, actual);\n" + return body diff --git a/generators/exercises/rail_fence_cipher.py b/generators/exercises/rail_fence_cipher.py new file mode 100644 index 00000000..6cede4fa --- /dev/null +++ b/generators/exercises/rail_fence_cipher.py @@ -0,0 +1,40 @@ +from lib import zstr, zint + +USE_MEM = True +IMPORT_SELF = True + +HEADER = """ +const encode = rail_fence_cipher.encode; +const decode = rail_fence_cipher.decode; + +const CipherFunc = *const fn (allocator: mem.Allocator, msg: []const u8, rails: u3) mem.Allocator.Error![]u8; + +fn railFenceCipherTest(allocator: mem.Allocator, cipherFunc: CipherFunc, msg: []const u8, rails: u3, expected: []const u8) anyerror!void { + const actual = try cipherFunc(allocator, msg, rails); + defer allocator.free(actual); + try testing.expectEqualStrings(expected, actual); +} +""" + + +def describe(case, parent): + # The canonical "encode"/"decode" group names are already present in each + # leaf description (e.g. "encode with two rails"), so drop the parent prefix. + return case["description"] + + +def gen_case(case): + prop = case["property"] # "encode" or "decode" + phrase = zstr(case["input"]["msg"]) + rails = zint(case["input"]["rails"]) + expect = zstr(case["expected"]) + lines = [ + f" const phrase: []const u8 = {phrase};", + f" const expect: []const u8 = {expect};", + " try std.testing.checkAllAllocationFailures(", + " std.testing.allocator,", + " railFenceCipherTest,", + f" .{{ &{prop}, phrase, {rails}, expect }},", + " );", + ] + return "\n".join(lines) + "\n" diff --git a/generators/exercises/rectangles.py b/generators/exercises/rectangles.py new file mode 100644 index 00000000..fbb7766e --- /dev/null +++ b/generators/exercises/rectangles.py @@ -0,0 +1,15 @@ +from lib import zstr, zint, zcomment_list + +USE_MEM = False + + +def gen_case(case): + strings = case["input"]["strings"] + expected = case["expected"] + # One row per line (each with a trailing `//`) so students can see the rectangle. + rows = zcomment_list([zstr(s) for s in strings]) + return ( + f" const strings = [_][]const u8{rows};\n" + f" const count = rectangles.rectangles(&strings);\n" + f" try testing.expectEqual({zint(expected)}, count);\n" + ) diff --git a/generators/exercises/resistor_color_trio.py b/generators/exercises/resistor_color_trio.py new file mode 100644 index 00000000..9336d435 --- /dev/null +++ b/generators/exercises/resistor_color_trio.py @@ -0,0 +1,26 @@ +from lib import zstr + +HEADER = ( + "const ColorBand = resistor_color_trio.ColorBand;\n" + "\n" + "fn labelTest(allocator: std.mem.Allocator, colors: []const ColorBand, expected: []const u8) anyerror!void {\n" + " const actual = try resistor_color_trio.label(allocator, colors);\n" + " defer testing.allocator.free(actual);\n" + " try testing.expectEqualStrings(expected, actual);\n" + "}\n" +) + + +def gen_case(case): + colors = ", ".join("." + c for c in case["input"]["colors"]) + expected = case["expected"] + label = f"{expected['value']} {expected['unit']}" + return ( + f" const colors = [_]ColorBand{{ {colors} }};\n" + f" const expected: []const u8 = {zstr(label)};\n" + f" try std.testing.checkAllAllocationFailures(\n" + f" std.testing.allocator,\n" + f" labelTest,\n" + f" .{{ &colors, expected }},\n" + f" );\n" + ) diff --git a/generators/exercises/reverse_string.py b/generators/exercises/reverse_string.py new file mode 100644 index 00000000..c4ab1f21 --- /dev/null +++ b/generators/exercises/reverse_string.py @@ -0,0 +1,13 @@ +from lib import zstr + +HEADER = """const buffer_size = 80; + +fn testReverse(comptime s: []const u8, expected: []const u8) !void { + var buffer: [buffer_size]u8 = undefined; + const actual = reverse_string.reverse(&buffer, s); + try testing.expectEqualStrings(expected, actual); +}""" + + +def gen_case(case): + return f" try testReverse({zstr(case['input']['value'])}, {zstr(case['expected'])});\n" diff --git a/generators/exercises/robot_simulator.py b/generators/exercises/robot_simulator.py new file mode 100644 index 00000000..cb97afec --- /dev/null +++ b/generators/exercises/robot_simulator.py @@ -0,0 +1,29 @@ +HEADER = """const Robot = robot_simulator.Robot; +""" + + +def gen_case(case): + inp = case["input"] + exp = case["expected"] + x = inp["position"]["x"] + y = inp["position"]["y"] + direction = inp["direction"] + ex = exp["position"]["x"] + ey = exp["position"]["y"] + edir = exp["direction"] + + out = [] + if case["property"] == "create": + out.append(f"const robot = Robot.init({x}, {y}, .{direction});\n") + out.append(f"try testing.expectEqual({ex}, robot.x);\n") + out.append(f"try testing.expectEqual({ey}, robot.y);\n") + out.append(f"try testing.expectEqual(.{edir}, robot.direction);\n") + return "".join(out) + + instructions = inp["instructions"] + out.append(f"var robot = Robot.init({x}, {y}, .{direction});\n") + out.append(f'robot.move("{instructions}");\n') + out.append(f"try testing.expectEqual({ex}, robot.x);\n") + out.append(f"try testing.expectEqual({ey}, robot.y);\n") + out.append(f"try testing.expectEqual(.{edir}, robot.direction);\n") + return "".join(out) diff --git a/generators/exercises/roman_numerals.py b/generators/exercises/roman_numerals.py new file mode 100644 index 00000000..64f2a918 --- /dev/null +++ b/generators/exercises/roman_numerals.py @@ -0,0 +1,14 @@ +IMPORT_SELF = False + +HEADER = 'const toRoman = @import("roman_numerals.zig").toRoman;' + + +def gen_case(case): + number = case["input"]["number"] + expected = case["expected"] + return ( + f' const expected = "{expected}";\n' + f" const actual = try toRoman(testing.allocator, {number});\n" + " defer testing.allocator.free(actual);\n" + " try testing.expectEqualStrings(expected, actual);\n" + ) diff --git a/generators/exercises/rotational_cipher.py b/generators/exercises/rotational_cipher.py new file mode 100644 index 00000000..ddc73fa7 --- /dev/null +++ b/generators/exercises/rotational_cipher.py @@ -0,0 +1,13 @@ +from lib import zstr, zint + + +def gen_case(case): + text = case["input"]["text"] + shift = case["input"]["shiftKey"] + expected = case["expected"] + return ( + f" const expected: []const u8 = {zstr(expected)};\n" + f" const actual = try rotational_cipher.rotate(testing.allocator, {zstr(text)}, {zint(shift)});\n" + f" defer testing.allocator.free(actual);\n" + f" try testing.expectEqualStrings(expected, actual);\n" + ) diff --git a/generators/exercises/run_length_encoding.py b/generators/exercises/run_length_encoding.py new file mode 100644 index 00000000..8861b9f6 --- /dev/null +++ b/generators/exercises/run_length_encoding.py @@ -0,0 +1,39 @@ +from lib import zstr + +HEADER = """ +fn testEncode(string: []const u8, expected: []const u8) !void { + const buffer_size = 80; + var buffer: [buffer_size]u8 = undefined; + const actual = run_length_encoding.encode(&buffer, string); + try testing.expectEqualStrings(expected, actual); +} + +fn testDecode(string: []const u8, expected: []const u8) !void { + const buffer_size = 80; + var buffer: [buffer_size]u8 = undefined; + const actual = run_length_encoding.decode(&buffer, string); + try testing.expectEqualStrings(expected, actual); +} + +fn testConsistency(string: []const u8, expected: []const u8) !void { + const buffer_size = 80; + var buffer1: [buffer_size]u8 = undefined; + var buffer2: [buffer_size]u8 = undefined; + const encoded = run_length_encoding.encode(&buffer1, string); + const actual = run_length_encoding.decode(&buffer2, encoded); + try testing.expectEqualStrings(expected, actual); +} +""" + +_FN = { + "encode": "testEncode", + "decode": "testDecode", + "consistency": "testConsistency", +} + + +def gen_case(case): + string = zstr(case["input"]["string"]) + expected = zstr(case["expected"]) + fn = _FN[case["property"]] + return f" try {fn}({string}, {expected});\n" diff --git a/generators/exercises/saddle_points.py b/generators/exercises/saddle_points.py new file mode 100644 index 00000000..0b58a4b6 --- /dev/null +++ b/generators/exercises/saddle_points.py @@ -0,0 +1,26 @@ +from lib import zcomment_list + +HEADER = "const saddlePoints = saddle_points.saddlePoints;\nconst Point = saddle_points.Point;\n" + + +def gen_case(case): + matrix = case["input"]["matrix"] + expected = case["expected"] + + m = len(matrix) + n = len(matrix[0]) if m else 0 + + rows = [f"[{n}]i32{{{', '.join(str(v) for v in row)}}}" for row in matrix] + matrix_lit = f"[{m}][{n}]i32{zcomment_list(rows)}" + + points = sorted(expected, key=lambda p: (p["row"], p["column"])) + point_elems = [f".{{ .row = {p['row']}, .column = {p['column']} }}" for p in points] + expected_lit = f"[_]Point{zcomment_list(point_elems)}" + + return ( + f" const matrix = {matrix_lit};\n" + f" const expected = {expected_lit};\n" + f" const actual = try saddlePoints({m}, {n}, testing.allocator, matrix);\n" + f" defer testing.allocator.free(actual);\n" + f" try testing.expectEqualSlices(Point, &expected, actual);\n" + ) diff --git a/generators/exercises/satellite.py b/generators/exercises/satellite.py new file mode 100644 index 00000000..63ca8292 --- /dev/null +++ b/generators/exercises/satellite.py @@ -0,0 +1,64 @@ +from lib import is_error + +HEADER = """const Tree = satellite.Tree; +const TraversalError = satellite.TraversalError; +""" + +_ERROR_MAP = { + "traversals must have the same length": "DifferentLengths", + "traversals must have the same elements": "DifferentItems", + "traversals must contain unique items": "NonUniqueItems", +} + + +def _u8_array(name, chars): + if not chars: + return f"const {name} = [_]u8{{}};\n" + inner = ", ".join(f"'{c}'" for c in chars) + return f"const {name} = [_]u8{{ {inner} }};\n" + + +def _traverse(expr, name, node, out): + if not node: + out.append(f"try testing.expectEqual(null, {expr});\n") + return + + if len(name) > 1 and name[0] == "o": + name = name[1:] + + out.append(f"if ({expr}) |{name}| {{\n") + out.append(f"try testing.expectEqual('{node['v']}', {name}.data);\n") + _traverse(name + ".left", name + "l", node.get("l"), out) + _traverse(name + ".right", name + "r", node.get("r"), out) + out.append("} else {\n") + out.append(f"try testing.expectEqual(false, true); // {expr} should not be null\n") + out.append("}\n") + + +def gen_case(case): + inp = case["input"] + expected = case["expected"] + preorder = inp["preorder"] + inorder = inp["inorder"] + + out = [_u8_array("preorder", preorder), _u8_array("inorder", inorder)] + + if is_error(expected): + err = _ERROR_MAP[expected["error"]] + out.append( + f"try testing.expectError(TraversalError.{err}, " + "Tree.initFromTraversals(testing.allocator, &preorder, &inorder));\n" + ) + return "".join(out) + + out.append( + "var tree = try Tree.initFromTraversals(testing.allocator, &preorder, &inorder);\n" + ) + out.append("defer tree.deinit();\n") + + if not expected: + out.append("try testing.expectEqual(null, tree.root);\n") + else: + _traverse("tree.root", "o", expected, out) + + return "".join(out) diff --git a/generators/exercises/say.py b/generators/exercises/say.py new file mode 100644 index 00000000..bd82840c --- /dev/null +++ b/generators/exercises/say.py @@ -0,0 +1,28 @@ +from lib import zint, is_error + +USE_MEM = True + +HEADER = """const SayError = say.SayError; + +fn sayTest(allocator: mem.Allocator, number: i41, expected: []const u8) anyerror!void { + const actual = try say.say(allocator, number); + defer allocator.free(actual); + try testing.expectEqualStrings(expected, actual); +}""" + + +def gen_case(case): + number = case["input"]["number"] + expected = case["expected"] + if is_error(expected): + return ( + f" try testing.expectError(SayError.OutOfRange, " + f"say.say(testing.allocator,{zint(number)}));\n" + ) + return ( + " try testing.checkAllAllocationFailures(\n" + " testing.allocator,\n" + " sayTest,\n" + f' .{{ {zint(number)}, "{expected}" }},\n' + " );\n" + ) diff --git a/generators/exercises/series.py b/generators/exercises/series.py new file mode 100644 index 00000000..32f5fc80 --- /dev/null +++ b/generators/exercises/series.py @@ -0,0 +1,38 @@ +from lib import zstr, is_error, zcomment_list + +# The canonical error cases are expressed as "expect an empty result" rather than `expectError`. +# +# Two uuids are excluded via `include = false` in tests.toml: +# 10ab822d-... "slice length cannot be negative" (track-specific) +# d34004ad-... "slice length cannot be zero" -- slice_length is comptime and the +# example asserts slice_length > 0 at comptime, so a 0-length call cannot compile. +IMPORT_SELF = False + +HEADER = 'const slices = @import("series.zig").slices;\n' + + +def gen_case(case): + inp = case["input"] + series_str = inp["series"] + n = inp["sliceLength"] + expected = case["expected"] + + if is_error(expected): + # Example returns an empty result for over-long slice lengths / empty series. + return ( + f" const series = {zstr(series_str)};\n" + f" const expected = [_][{n}]u8{{}};\n" + f" const actual = try slices({n}, testing.allocator, series);\n" + f" defer testing.allocator.free(actual);\n" + f" try testing.expectEqualSlices([{n}]u8, &expected, actual);\n" + ) + + # One slice per line (with trailing `//`) so the expected series stays readable. + elems = zcomment_list([f"{zstr(s)}[0..{n}].*" for s in expected]) + return ( + f" const series = {zstr(series_str)};\n" + f" const expected = [_][{n}]u8{elems};\n" + f" const actual = try slices({n}, testing.allocator, series);\n" + f" defer testing.allocator.free(actual);\n" + f" try testing.expectEqualSlices([{n}]u8, &expected, actual);\n" + ) diff --git a/generators/exercises/spiral_matrix.py b/generators/exercises/spiral_matrix.py new file mode 100644 index 00000000..1c4cef66 --- /dev/null +++ b/generators/exercises/spiral_matrix.py @@ -0,0 +1,40 @@ +IMPORT_SELF = True + +HEADER = """const spiral = spiral_matrix.spiral; + +fn free(slices: [][]u16) void { + for (slices) |slice| { + testing.allocator.free(slice); + } + testing.allocator.free(slices); +} + +fn spiralTest(allocator: std.mem.Allocator, size: u16, expected: [][]const u16) anyerror!void { + const actual = try spiral(allocator, size); + defer free(actual); + + try testing.expectEqual(expected.len, actual.len); + for (expected, actual) |expected_slice, actual_slice| { + try testing.expectEqualSlices(u16, expected_slice, actual_slice); + } +} +""" + + +def gen_case(case): + size = case["input"]["size"] + rows = case["expected"] + count = len(rows) + if count == 0: + body = " const expected: [0][]const u16 = undefined;\n" + else: + body = f" var expected: [{count}][]const u16 = undefined;\n" + for i, row in enumerate(rows): + inner = ", ".join(str(v) for v in row) + body += f" expected[{i}] = &.{{{inner}}};\n" + body += " try testing.checkAllAllocationFailures(\n" + body += " testing.allocator,\n" + body += " spiralTest,\n" + body += f" .{{ {size}, &expected }},\n" + body += " );\n" + return body diff --git a/generators/exercises/split_second_stopwatch.py b/generators/exercises/split_second_stopwatch.py new file mode 100644 index 00000000..4daf2868 --- /dev/null +++ b/generators/exercises/split_second_stopwatch.py @@ -0,0 +1,62 @@ +HEADER = """const Stopwatch = split_second_stopwatch.Stopwatch; +const StopwatchError = split_second_stopwatch.StopwatchError; +const StopwatchState = split_second_stopwatch.StopwatchState; +""" + +_ERROR_MAP = { + "cannot start an already running stopwatch": "AlreadyRunning", + "cannot stop a stopwatch that is not running": "NotRunning", + "cannot lap a stopwatch that is not running": "NotRunning", + "cannot reset a stopwatch that is not stopped": "NotStopped", +} + + +def _is_error(expected): + return isinstance(expected, dict) and "error" in expected + + +def gen_case(case): + commands = case["input"]["commands"] + uses_previous = any(c["command"] == "previousLaps" for c in commands) + + out = [] + if uses_previous: + out.append("var previous: [][8]u8 = undefined;\n") + + for cmd in commands: + name = cmd["command"] + expected = cmd.get("expected") + + if name == "new": + out.append("var stopwatch = Stopwatch.init(testing.allocator);\n") + out.append("defer stopwatch.deinit();\n") + elif name == "state": + out.append(f"try testing.expectEqual(.{expected}, stopwatch.state);\n") + elif name == "currentLap": + out.append( + f'try testing.expectEqualStrings("{expected}", &stopwatch.currentLap());\n' + ) + elif name == "total": + out.append( + f'try testing.expectEqualStrings("{expected}", &stopwatch.total());\n' + ) + elif name == "advanceTime": + out.append(f'try stopwatch.advanceTime("{cmd["by"]}");\n') + elif name == "previousLaps": + out.append("previous = try stopwatch.previousLaps();\n") + out.append(f"try testing.expectEqual({len(expected)}, previous.len);\n") + for i, lap in enumerate(expected): + out.append( + f'try testing.expectEqualStrings("{lap}", &previous[{i}]);\n' + ) + out.append("testing.allocator.free(previous);\n") + elif name in ("start", "stop", "reset", "lap"): + if _is_error(expected): + err = _ERROR_MAP[expected["error"]] + out.append( + f"try testing.expectError(StopwatchError.{err}, stopwatch.{name}());\n" + ) + else: + out.append(f"try stopwatch.{name}();\n") + + return "".join(out) diff --git a/generators/exercises/square_root.py b/generators/exercises/square_root.py new file mode 100644 index 00000000..28243fe7 --- /dev/null +++ b/generators/exercises/square_root.py @@ -0,0 +1,8 @@ +def gen_case(case): + radicand = case["input"]["radicand"] + expected = case["expected"] + return ( + f" const expected: usize = {expected};\n" + f" const actual = square_root.squareRoot({radicand});\n" + f" try testing.expectEqual(expected, actual);\n" + ) diff --git a/generators/exercises/state_of_tic_tac_toe.py b/generators/exercises/state_of_tic_tac_toe.py new file mode 100644 index 00000000..c37de3fc --- /dev/null +++ b/generators/exercises/state_of_tic_tac_toe.py @@ -0,0 +1,25 @@ +from lib import zstr + +USE_MEM = False +IMPORT_SELF = True + +HEADER = """ +const GameState = state_of_tic_tac_toe.GameState; +""" + + +def gen_case(case): + rows = case["input"]["board"] + exp = case["expected"] + lines = [" const board = [_][]const u8{"] + for row in rows: + lines.append(f" {zstr(row)}, //") + lines.append(" };") + lines.append(" const actual = state_of_tic_tac_toe.gameState(&board);") + if isinstance(exp, dict) and "error" in exp: + # An invalid board maps to `.impossible`; note why via the canonical message. + lines.append(f" // {exp['error']}") + lines.append(" try testing.expectEqual(GameState.impossible, actual);") + else: + lines.append(f" try testing.expectEqual(GameState.{exp}, actual);") + return "\n".join(lines) + "\n" diff --git a/generators/exercises/sublist.py b/generators/exercises/sublist.py new file mode 100644 index 00000000..d056a8ab --- /dev/null +++ b/generators/exercises/sublist.py @@ -0,0 +1,18 @@ +def _ints(values): + if not values: + return "&[_]i32{}" + inner = ", ".join(str(v) for v in values) + return f"&[_]i32{{ {inner} }}" + + +def gen_case(case): + list_one = case["input"]["listOne"] + list_two = case["input"]["listTwo"] + expected = case["expected"] # e.g. "equal", "sublist" + return ( + f" const list_one = {_ints(list_one)};\n" + f" const list_two = {_ints(list_two)};\n" + f" const expected = .{expected};\n" + f" const actual = sublist.compare(list_one, list_two);\n" + f" try testing.expectEqual(expected, actual);\n" + ) diff --git a/generators/exercises/transpose.py b/generators/exercises/transpose.py new file mode 100644 index 00000000..5ad16218 --- /dev/null +++ b/generators/exercises/transpose.py @@ -0,0 +1,45 @@ +from lib import zstr + +IMPORT_SELF = False + +HEADER = """const transpose = @import("transpose.zig").transpose; + +fn free(slices: [][]u8) void { + for (slices) |slice| { + testing.allocator.free(slice); + } + testing.allocator.free(slices); +} + +fn testTranspose(allocator: std.mem.Allocator, expected: []const []const u8, lines: []const []const u8) !void { + const actual = try transpose(allocator, lines); + defer free(actual); + try testing.expectEqual(expected.len, actual.len); + for (0..expected.len) |i| { + try testing.expectEqualStrings(expected[i], actual[i]); + } +} +""" + + +def _array(name, items): + if not items: + return f" const {name} = [_][]const u8{{}};\n" + body = f" const {name} = [_][]const u8{{\n" + for item in items: + body += f" {zstr(item)}, //\n" + body += " };\n" + return body + + +def gen_case(case): + lines = case["input"]["lines"] + expected = case["expected"] + body = _array("lines", lines) + body += _array("expected", expected) + body += " try std.testing.checkAllAllocationFailures(\n" + body += " std.testing.allocator,\n" + body += " testTranspose,\n" + body += " .{ &expected, &lines },\n" + body += " );\n" + return body diff --git a/generators/exercises/twelve_days.py b/generators/exercises/twelve_days.py new file mode 100644 index 00000000..768b4e90 --- /dev/null +++ b/generators/exercises/twelve_days.py @@ -0,0 +1,16 @@ +from lib import zmultiline + + +def gen_case(case): + inp = case["input"] + start, end = inp["startVerse"], inp["endVerse"] + expected = zmultiline("\n".join(case["expected"])) + return ( + " const buffer_size = 4000;\n" + " var buffer: [buffer_size]u8 = undefined;\n" + " const expected: []const u8 =\n" + f"{expected}\n" + " ;\n" + f" const actual = try twelve_days.recite(&buffer, {start}, {end});\n" + " try testing.expectEqualStrings(expected, actual);\n" + ) diff --git a/generators/exercises/two_bucket.py b/generators/exercises/two_bucket.py new file mode 100644 index 00000000..a3de811d --- /dev/null +++ b/generators/exercises/two_bucket.py @@ -0,0 +1,26 @@ +from lib import is_error + +HEADER = "const measure = two_bucket.measure;\n" + + +def gen_case(case): + inp = case["input"] + one = inp["bucketOne"] + two = inp["bucketTwo"] + goal = inp["goal"] + start = inp["startBucket"] + call = f"measure({one}, {two}, {goal}, .{start})" + expected = case["expected"] + if is_error(expected): + return ( + f" const result = {call};\n try testing.expectEqual(result, null);\n" + ) + return ( + f" if ({call}) |result| {{\n" + f" try testing.expectEqual({expected['moves']}, result.moves);\n" + f" try testing.expectEqual(.{expected['goalBucket']}, result.goal_bucket);\n" + f" try testing.expectEqual({expected['otherBucket']}, result.other_bucket);\n" + " } else {\n" + " try testing.expect(false);\n" + " }\n" + ) diff --git a/generators/exercises/variable_length_quantity.py b/generators/exercises/variable_length_quantity.py new file mode 100644 index 00000000..f1d57735 --- /dev/null +++ b/generators/exercises/variable_length_quantity.py @@ -0,0 +1,45 @@ +from lib import zint, is_error + +HEADER = ( + "const encode = variable_length_quantity.encode;\n" + "const decode = variable_length_quantity.decode;\n" + "const DecodeError = variable_length_quantity.DecodeError;\n" +) + + +def describe(case, parent): + # Special join: " - " rather than the default dash join. + desc = case["description"] + prop = case.get("property") + if prop and "cases" not in case: + return f"{prop} - {desc}" + return desc + + +def gen_case(case): + prop = case["property"] + integers = case["input"]["integers"] + expected = case["expected"] + in_ty = "u32" if prop == "encode" else "u8" + integers_lit = ( + "{ " + ", ".join(zint(v) for v in integers) + " }" if integers else "{}" + ) + + if is_error(expected): + return ( + f" const integers = [_]{in_ty}{integers_lit};\n" + f" const actual = {prop}(testing.allocator, &integers);\n" + f" try testing.expectError(DecodeError.IncompleteSequence, actual);\n" + ) + + out_ty = "u8" if prop == "encode" else "u32" + expected_lit = ( + "{ " + ", ".join(zint(v) for v in expected) + " }" if expected else "{}" + ) + return ( + f" const expected = [_]{out_ty}{expected_lit};\n" + f" const integers = [_]{in_ty}{integers_lit};\n" + f" const actual = try {prop}(testing.allocator, &integers);\n" + f" defer testing.allocator.free(actual);\n" + f" try testing.expectEqualSlices({out_ty}, &expected, actual);\n" + ) diff --git a/generators/exercises/word_count.py b/generators/exercises/word_count.py new file mode 100644 index 00000000..f3e1c15f --- /dev/null +++ b/generators/exercises/word_count.py @@ -0,0 +1,44 @@ +from lib import zstr + +IMPORT_SELF = False + +HEADER = """const countWords = @import("word_count.zig").countWords; + +fn freeKeysAndDeinit(self: *std.StringHashMap(u32)) void { + var iter = self.keyIterator(); + while (iter.next()) |key_ptr| { + self.allocator.free(key_ptr.*); + } + self.deinit(); +} + +fn countWordsTestFn(allocator: std.mem.Allocator, s: []const u8) anyerror!void { + var map = try countWords(allocator, s); + defer freeKeysAndDeinit(&map); +} + +fn countWordsCheckAllocationFailures(allocator: std.mem.Allocator, s: []const u8) anyerror!void { + std.testing.checkAllAllocationFailures( + allocator, + countWordsTestFn, + .{s}, + ) catch |err| switch (err) { + error.SwallowedOutOfMemoryError => return, + else => return err, + }; +} +""" + + +def gen_case(case): + s = case["input"]["sentence"] + expected = case["expected"] + count = len(expected) + body = f" const s = {zstr(s)};\n" + body += " var map = try countWords(testing.allocator, s);\n" + body += " defer freeKeysAndDeinit(&map);\n" + body += f" try testing.expectEqual(@as(u32, {count}), map.count());\n" + for word, n in expected.items(): + body += f" try testing.expectEqual(@as(?u32, {n}), map.get({zstr(word)}));\n" + body += " try countWordsCheckAllocationFailures(testing.allocator, s);\n" + return body diff --git a/generators/exercises/wordy.py b/generators/exercises/wordy.py new file mode 100644 index 00000000..cb2e27f2 --- /dev/null +++ b/generators/exercises/wordy.py @@ -0,0 +1,20 @@ +from lib import zstr, is_error + +HEADER = "const answer = wordy.answer;\nconst ArgumentError = wordy.ArgumentError;\n" + +# Canonical error message -> ArgumentError +_ERROR_MAP = { + "unknown operation": "UnsupportedQuestion", + "syntax error": "SyntaxError", + "division by zero": "DivisionByZero", +} + + +def gen_case(case): + question = case["input"]["question"] + expected = case["expected"] + q = zstr(question) + if is_error(expected): + variant = _ERROR_MAP[expected["error"]] + return f" try testing.expectError(ArgumentError.{variant}, answer({q}));\n" + return f" try testing.expectEqual({expected}, answer({q}));\n" diff --git a/generators/exercises/zebra_puzzle.py b/generators/exercises/zebra_puzzle.py new file mode 100644 index 00000000..c7ce2a25 --- /dev/null +++ b/generators/exercises/zebra_puzzle.py @@ -0,0 +1,18 @@ +HEADER = """const solve = zebra_puzzle.solve; +const Nationality = zebra_puzzle.Nationality;""" + +# canonical property name -> Solution struct field accessed on the solution +_FIELDS = { + "drinksWater": "drinks_water", + "ownsZebra": "owns_zebra", +} + + +def gen_case(case): + field = _FIELDS[case["property"]] + nationality = case["expected"].lower() + return ( + f" const expected: Nationality = .{nationality};\n" + f" const actual = (try solve(testing.allocator)).{field};\n" + " try testing.expectEqual(expected, actual);\n" + ) diff --git a/generators/generate.py b/generators/generate.py new file mode 100644 index 00000000..091d60a6 --- /dev/null +++ b/generators/generate.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python3 +"""Zig track test generator. + +Architecture: + * This orchestrator owns everything generic: locating canonical data, flattening the + case tree, honoring `reimplements`/unicode scenarios and `.meta/tests.toml`, appending + `.meta/supplements.json`, wrapping each case in a `test "..." {}` block, + assembling the import header, and running `zig fmt`. + * generators/lib.py owns Zig value formatting (escaping, int grouping, slice literals). + * generators/exercises/.py owns ONLY the per-case body and any exercise-specific + header (helper fns / const aliases). + +Per-exercise module surface (all optional except gen_case): + USE_MEM = False # add `const mem = std.mem;` + IMPORT_SELF = True # add `const = @import(".zig");` + HEADER = "" # extra lines after imports (const aliases, helper fns) + def describe(case, parent): -> str # custom test-name; default joins with '-' + def gen_case(case): -> str # body of the test block (required) + +Usage: + bin/generate [ ...] # generate listed exercises + bin/generate --all # generate every exercise with a module + bin/generate --check ... # verify generated file matches committed +""" + +import argparse +import importlib +import json +import os +import subprocess +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(HERE) +sys.path.insert(0, HERE) + +ZIG = "zig" + + +def read_canonical_data(slug): + prefix = "Using cached 'problem-specifications' dir: " + info = subprocess.run( + ["bin/configlet", "info", "-o", "-v", "d"], + capture_output=True, + check=True, + text=True, + cwd=ROOT, + ).stdout.split("\n") + cache = [ln[len(prefix) :] for ln in info if ln.startswith(prefix)] + if len(cache) != 1: + raise SystemExit("Could not determine 'problem-specifications' dir") + path = f"{cache[0]}/exercises/{slug}/canonical-data.json" + with open(path) as f: + return json.load(f) + + +def collect_reimplemented(data): + """uuids to exclude: explicit `reimplements` targets + unicode-scenario cases.""" + out = set() + + def walk(node): + if "cases" in node: + for c in node["cases"]: + walk(c) + else: + if "reimplements" in node: + out.add(node["reimplements"]) + if "unicode" in node.get("scenarios", []): + out.add(node["uuid"]) + + walk(data) + return out + + +def default_describe(case, parent): + desc = case["description"] + if parent: + desc = f"{parent}-{desc}" + desc = desc.replace("garden-garden", "garden") + return desc + + +def flatten(data, mod, reimplemented, toml): + """Flatten the case tree into leaf cases, attaching a joined `description`.""" + describe = getattr(mod, "describe", default_describe) + leaves = [] + + def walk(node, parent): + desc = describe(node, parent) + if "cases" in node: + for c in node["cases"]: + walk(c, desc) + return + uuid = node.get("uuid") + if uuid in reimplemented: + return + if toml.get(uuid, {}).get("include", True) is False: + return + leaves.append({**node, "description": desc}) + + for node in data["cases"]: + # top-level group nodes may lack a uuid + walk(node, None) + return leaves + + +def load_toml(slug): + path = f"{ROOT}/exercises/practice/{slug}/.meta/tests.toml" + if not os.path.exists(path): + return {} + try: + import tomllib + except ModuleNotFoundError: + import tomli as tomllib + with open(path, "rb") as f: + return tomllib.load(f) + + +def load_supplements(slug): + path = f"{ROOT}/exercises/practice/{slug}/.meta/supplements.json" + if not os.path.exists(path): + return [] + with open(path) as f: + return json.load(f).get("cases", []) + + +def render(slug, mod, leaves): + us = slug.replace("-", "_") + parts = ['const std = @import("std");\n'] + if getattr(mod, "USE_MEM", False): + parts.append("const mem = std.mem;\n") + parts.append("const testing = std.testing;\n\n") + if getattr(mod, "IMPORT_SELF", True): + parts.append(f'const {us} = @import("{us}.zig");\n') + header = getattr(mod, "HEADER", "") + if header: + parts.append(header if header.endswith("\n") else header + "\n") + body = "".join(parts) + for case in leaves: + inner = mod.gen_case(case) + body += "\n" + f'test "{case["description"]}" ' + "{\n" + inner + if not inner.endswith("\n"): + body += "\n" + body += "}\n" + return body + + +def zig_fmt(text): + try: + r = subprocess.run( + [ZIG, "fmt", "--stdin"], input=text, capture_output=True, text=True + ) + if r.returncode == 0: + return r.stdout + sys.stderr.write( + f"warning: `zig fmt` failed, writing unformatted:\n{r.stderr}\n" + ) + except FileNotFoundError: + sys.stderr.write("warning: `zig` not found; writing unformatted\n") + return text + + +def generate(slug, check=False): + mod = importlib.import_module("exercises." + slug.replace("-", "_")) + data = read_canonical_data(slug) + reimplemented = collect_reimplemented(data) + leaves = flatten(data, mod, reimplemented, load_toml(slug)) + for extra in load_supplements(slug): + # supplements may themselves be nested groups + def walk(node, parent): + desc = getattr(mod, "describe", default_describe)(node, parent) + if "cases" in node: + for c in node["cases"]: + walk(c, desc) + else: + leaves.append({**node, "description": desc}) + + walk(extra, None) + + # Supplements always append after canonical cases; an optional `order_key` lets a + # module re-sort the combined list into a more natural order (stable sort). + if hasattr(mod, "order_key"): + leaves.sort(key=mod.order_key) + + text = zig_fmt(render(slug, mod, leaves)) + us = slug.replace("-", "_") + out = f"{ROOT}/exercises/practice/{slug}/test_{us}.zig" + if check: + existing = open(out).read() if os.path.exists(out) else "" + if existing != text: + print(f"DRIFT: {slug}") + return False + print(f"ok: {slug}") + return True + os.makedirs(os.path.dirname(out), exist_ok=True) + with open(out, "w") as f: + f.write(text) + print(f"generated: exercises/practice/{slug}/test_{us}.zig") + return True + + +def all_slugs(): + d = f"{HERE}/exercises" + return sorted( + f[:-3].replace("_", "-") + for f in os.listdir(d) + if f.endswith(".py") and f != "__init__.py" + ) + + +def main(): + p = argparse.ArgumentParser() + p.add_argument("slugs", nargs="*") + p.add_argument("--all", action="store_true") + p.add_argument("--check", action="store_true") + args = p.parse_args() + slugs = all_slugs() if args.all else args.slugs + if not slugs: + p.error("provide exercise slug(s) or --all") + ok = True + for slug in slugs: + try: + ok = generate(slug, check=args.check) and ok + except Exception as e: + ok = False + print(f"ERROR {slug}: {e}", file=sys.stderr) + sys.exit(0 if ok else 1) + + +if __name__ == "__main__": + main() diff --git a/generators/lib.py b/generators/lib.py new file mode 100644 index 00000000..0b620f62 --- /dev/null +++ b/generators/lib.py @@ -0,0 +1,109 @@ +"""Shared Zig value formatters for the test generator. + +This module owns every *language-level* value formatting primitive, so that per-exercise +modules never re-implement escaping, integer grouping, bool spelling, or slice literals. + +A per-exercise module receives a `case` dict (keys: property, description, input, +expected, uuid) and returns the *body* of a `test "..." { ... }` block as a string. +`zig fmt` is run on the final file, so module output need not be perfectly indented. +""" + + +def zint(n): + """Integer literal with Zig '_' digit grouping, e.g. 1_000_000. + + Matches the reference generator's f"{n:_}" formatting. + """ + return format(int(n), "_") + + +def zbool(b): + """Python bool -> Zig bool literal.""" + return "true" if b else "false" + + +def zfloat(x): + """Float literal.""" + return repr(float(x)) + + +# Characters needing escapes inside a Zig "..." string literal. +_STR_ESCAPES = { + "\\": "\\\\", + '"': '\\"', + "\n": "\\n", + "\r": "\\r", + "\t": "\\t", +} + + +def zstr(s): + """A double-quoted Zig string literal. + + Printable ASCII and printable Unicode are kept raw (Zig source is UTF-8, so a literal + like "brühe" stays readable); only control characters and DEL are emitted as \\xNN + escapes. + """ + out = ['"'] + for ch in s: + if ch in _STR_ESCAPES: + out.append(_STR_ESCAPES[ch]) + elif 0x20 <= ord(ch) < 0x7F or ord(ch) >= 0xA0: + out.append(ch) + else: + for b in ch.encode("utf-8"): + out.append(f"\\x{b:02x}") + out.append('"') + return "".join(out) + + +def zcomment_list(elements): + """Array body with one element per line, each followed by an empty `//` comment. + + The trailing `//` stops `zig fmt` from collapsing the array onto one line, so + multi-line shapes (rectangle grids, slice lists) stay readable. Returns e.g. + "{\\n a, //\\n b, //\\n}"; pair it with a `[_]T` prefix. + """ + if not elements: + return "{}" + body = "".join(f"\n {e}, //" for e in elements) + return "{" + body + "\n}" + + +def zmultiline(s): + """A Zig multiline string literal (``\\\\`` line prefixes) for text with newlines. + + Produces, for "a\\nb": + \\\\a + \\\\b + Used by recitation-style exercises (bottle-song, etc.). No escaping of inner + characters is needed in multiline literals except that each line is prefixed. + """ + lines = s.split("\n") + return "\n".join("\\\\" + line for line in lines) + + +def zslice(values, elem_ty="i32"): + """A Zig slice literal: &[_]{ a, b, c } (handles empty).""" + if len(values) == 0: + return f"&[_]{elem_ty}{{}}" + inner = ", ".join(str(v) for v in values) + return f"&[_]{elem_ty}{{ {inner} }}" + + +def zarray(values, elem_ty="i32"): + """A by-value Zig array literal: [_]{ a, b, c }.""" + if len(values) == 0: + return f"[_]{elem_ty}{{}}" + inner = ", ".join(str(v) for v in values) + return f"[_]{elem_ty}{{ {inner} }}" + + +def zint_slice(values, elem_ty="i32"): + """Slice literal from a list of ints, applying '_' grouping to each element.""" + return zslice([zint(v) for v in values], elem_ty) + + +def is_error(expected): + """True if a canonical `expected` denotes an error case ({"error": "..."}).""" + return isinstance(expected, dict) and "error" in expected