diff --git a/config.json b/config.json index e2fe735..8e38d2c 100644 --- a/config.json +++ b/config.json @@ -396,6 +396,14 @@ "practices": [], "prerequisites": [], "difficulty": 6 + }, + { + "slug": "two-bucket", + "name": "Two Bucket", + "uuid": "29306fb4-5841-4724-a3cc-d5b29d203a7a", + "practices": [], + "prerequisites": [], + "difficulty": 7 } ] }, diff --git a/exercises/practice/two-bucket/.docs/instructions.md b/exercises/practice/two-bucket/.docs/instructions.md new file mode 100644 index 0000000..30d779a --- /dev/null +++ b/exercises/practice/two-bucket/.docs/instructions.md @@ -0,0 +1,46 @@ +# Instructions + +Given two buckets of different size and which bucket to fill first, determine how many actions are required to measure an exact number of liters by strategically transferring fluid between the buckets. + +There are some rules that your solution must follow: + +- You can only do one action at a time. +- There are only 3 possible actions: + 1. Pouring one bucket into the other bucket until either: + a) the first bucket is empty + b) the second bucket is full + 2. Emptying a bucket and doing nothing to the other. + 3. Filling a bucket and doing nothing to the other. +- After an action, you may not arrive at a state where the initial starting bucket is empty and the other bucket is full. + +Your program will take as input: + +- the size of bucket one +- the size of bucket two +- the desired number of liters to reach +- which bucket to fill first, either bucket one or bucket two + +Your program should determine: + +- the total number of actions it should take to reach the desired number of liters, including the first fill of the starting bucket +- which bucket should end up with the desired number of liters - either bucket one or bucket two +- how many liters are left in the other bucket + +Note: any time a change is made to either or both buckets counts as one (1) action. + +Example: +Bucket one can hold up to 7 liters, and bucket two can hold up to 11 liters. +Let's say at a given step, bucket one is holding 7 liters and bucket two is holding 8 liters (7,8). +If you empty bucket one and make no change to bucket two, leaving you with 0 liters and 8 liters respectively (0,8), that counts as one action. +Instead, if you had poured from bucket one into bucket two until bucket two was full, resulting in 4 liters in bucket one and 11 liters in bucket two (4,11), that would also only count as one action. + +Another Example: +Bucket one can hold 3 liters, and bucket two can hold up to 5 liters. +You are told you must start with bucket one. +So your first action is to fill bucket one. +You choose to empty bucket one for your second action. +For your third action, you may not fill bucket two, because this violates the third rule -- you may not end up in a state after any action where the starting bucket is empty and the other bucket is full. + +Written with <3 at [Fullstack Academy][fullstack] by Lindsay Levine. + +[fullstack]: https://www.fullstackacademy.com/ diff --git a/exercises/practice/two-bucket/.meta/config.json b/exercises/practice/two-bucket/.meta/config.json new file mode 100644 index 0000000..74830a8 --- /dev/null +++ b/exercises/practice/two-bucket/.meta/config.json @@ -0,0 +1,21 @@ +{ + "authors": [ + "IsaacG" + ], + "files": { + "solution": [ + "two_bucket.gd" + ], + "test": [ + "two_bucket_test.gd", + "run_tests", + "lib/test_runner.gd" + ], + "example": [ + ".meta/example.gd" + ] + }, + "blurb": "Given two buckets of different size, demonstrate how to measure an exact number of liters.", + "source": "Water Pouring Problem", + "source_url": "https://demonstrations.wolfram.com/WaterPouringProblem/" +} diff --git a/exercises/practice/two-bucket/.meta/example.gd b/exercises/practice/two-bucket/.meta/example.gd new file mode 100644 index 0000000..546b985 --- /dev/null +++ b/exercises/practice/two-bucket/.meta/example.gd @@ -0,0 +1,76 @@ +"""Solve the jug filling problem.""" + +class Bucket: + """A bucket with a capacity, volume and move counter.""" + + var volume = 0 + var capacity = 0 + var moves = 0 + var name = "" + + func _init(capacity: int, name: String): + """Initialize.""" + self.capacity = capacity + self.name = name + + func fill_if_empty(): + """Fill the bucket if it is empty.""" + if self.volume == 0: + self.volume = self.capacity + self.moves += 1 + + func empty_if_full(): + """Empty the bucket if it is full.""" + if self.volume == self.capacity: + self.volume = 0 + self.moves += 1 + + func transfer_to(other): + """Pour water from self into another bucket.""" + var amount = min(self.volume, other.capacity - other.volume) + if amount == 0: + return + self.volume -= amount + other.volume += amount + self.moves += 1 + + +func measure(one: int, two: int, goal: int, start: String): + """Pour from source to dest bucket until we have the goal volume.""" + if not one or not two or not goal or start not in ["one", "two"]: + return null + # Create and name the two buckets. + var source = Bucket.new(one, "one") + var dest = Bucket.new(two, "two") + # If starting with bucket two, swap pour direction. + if start == "two": + var i = dest + dest = source + source = i + + # The first move must always be filling the source. + source.fill_if_empty() + + # Edge case: if goal == dest bucket size, fill the dest. + if goal == dest.capacity: + dest.fill_if_empty() + + # Start the pouring! Stop when goal is hit. + while goal not in [source.volume, dest.volume]: + # In order to transfer, the source needs water and the dest needs space. + source.fill_if_empty() + dest.empty_if_full() + if source.volume == source.capacity and dest.volume == 0 and dest.moves: + return null + source.transfer_to(dest) + + # Figure out which bucket has what. One is at the "target" volume. + var target = dest + var other = source + if source.volume == goal: + var c = target + target = other + other = c + var moves = source.moves + dest.moves + return {"moves": moves, "goal_bucket": target.name, "other_bucket": other.volume} + diff --git a/exercises/practice/two-bucket/.meta/template.j2 b/exercises/practice/two-bucket/.meta/template.j2 new file mode 100644 index 0000000..e14ab8e --- /dev/null +++ b/exercises/practice/two-bucket/.meta/template.j2 @@ -0,0 +1,16 @@ +{% for case in cases %} +func test_{{ case.func_name }}(solution_script): +{%- if not case.expect_error %} + var want = { + "moves": {{ case.expected.moves }}, + "goal_bucket": {{ case.expected.goalBucket | togdscript }}, + "other_bucket": {{ case.expected.otherBucket }}, + } +{%- else %} + var want = null +{%- endif %} + var got = solution_script.{{ case.property }}({{ case.input.bucketOne }}, {{ case.input.bucketTwo }}, {{ case.input.goal }}, {{ case.input.startBucket | togdscript }}) + return [got, want] + +{% endfor %} + diff --git a/exercises/practice/two-bucket/.meta/tests.toml b/exercises/practice/two-bucket/.meta/tests.toml new file mode 100644 index 0000000..a3fe533 --- /dev/null +++ b/exercises/practice/two-bucket/.meta/tests.toml @@ -0,0 +1,43 @@ +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[a6f2b4ba-065f-4dca-b6f0-e3eee51cb661] +description = "Measure using bucket one of size 3 and bucket two of size 5 - start with bucket one" + +[6c4ea451-9678-4926-b9b3-68364e066d40] +description = "Measure using bucket one of size 3 and bucket two of size 5 - start with bucket two" + +[3389f45e-6a56-46d5-9607-75aa930502ff] +description = "Measure using bucket one of size 7 and bucket two of size 11 - start with bucket one" + +[fe0ff9a0-3ea5-4bf7-b17d-6d4243961aa1] +description = "Measure using bucket one of size 7 and bucket two of size 11 - start with bucket two" + +[0ee1f57e-da84-44f7-ac91-38b878691602] +description = "Measure one step using bucket one of size 1 and bucket two of size 3 - start with bucket two" + +[eb329c63-5540-4735-b30b-97f7f4df0f84] +description = "Measure using bucket one of size 2 and bucket two of size 3 - start with bucket one and end with bucket two" + +[58d70152-bf2b-46bb-ad54-be58ebe94c03] +description = "Measure using bucket one much bigger than bucket two" + +[9dbe6499-caa5-4a58-b5ce-c988d71b8981] +description = "Measure using bucket one much smaller than bucket two" + +[449be72d-b10a-4f4b-a959-ca741e333b72] +description = "Not possible to reach the goal" + +[aac38b7a-77f4-4d62-9b91-8846d533b054] +description = "With the same buckets but a different goal, then it is possible" + +[74633132-0ccf-49de-8450-af4ab2e3b299] +description = "Goal larger than both buckets is impossible" diff --git a/exercises/practice/two-bucket/lib/test_runner.gd b/exercises/practice/two-bucket/lib/test_runner.gd new file mode 100644 index 0000000..9274583 --- /dev/null +++ b/exercises/practice/two-bucket/lib/test_runner.gd @@ -0,0 +1,467 @@ +class_name TestRunner +extends SceneTree + +class ErrorReader: + """ErrorReader reads from a STDERR file (cmd 2>/file). + + It filters out the less useful Godot output and handles retrieving lines iteratively. + """ + + const EXPECTED_GODOT_ERRORS = [ + "ERROR: Could not create directory: /root/.local\n at: make_dir_recursive (core/io/dir_access.cpp:180)\n", + "ERROR: Error attempting to create data dir: /root/.local/share/godot/app_userdata/[unnamed project].\n at: ensure_user_data_dir (core/os/os.cpp:344)\n", + "ERROR: Could not create editor data directory: /root/.local/share/godot\n at: EditorPaths (editor/editor_paths.cpp:183)\n", + "ERROR: Could not create directory: /root/.config\n at: make_dir_recursive (core/io/dir_access.cpp:180)\n", + "ERROR: Could not create editor config directory: /root/.config/godot\n at: EditorPaths (editor/editor_paths.cpp:198)\n", + "ERROR: Could not create directory: /root/.cache\n at: make_dir_recursive (core/io/dir_access.cpp:180)\n", + "ERROR: Could not create editor cache directory: /root/.cache/godot\n at: EditorPaths (editor/editor_paths.cpp:219)\n", + "ERROR: Can't save resource to empty path. Provide non-empty path or a Resource with non-empty resource_path.\n at: save (core/io/resource_saver.cpp:105)\n", + "ERROR: Error saving editor settings to \n at: save (editor/editor_settings.cpp:1013)\n", + ] + var already_processed_bytes: int = 0 + var filename: String = "" + var color_sequence = RegEx.create_from_string("\\e\\[\\d[\\d;]*m") + + func _init(filename: String) -> void: + self.already_processed_bytes = 0 + self.filename = filename + if not self.filename.is_empty() and ResourceLoader.exists(self.filename): + self.get_error_message() + + func check() -> Error: + if not FileAccess.file_exists(self.filename): + push_error("STDERR must be redirected to '%s' when using '--json'" % self.filename) + return ERR_FILE_NOT_FOUND + if OS.get_stderr_type() != OS.STD_HANDLE_FILE: + push_error("STDERR must be redirected to '%s' when using '--json'" % self.filename) + return ERR_FILE_NOT_FOUND + return OK + + func get_error_message() -> String: + """ + Checks the contents of the STDERR file, where error output from the + currently running test suite should be stored. If there is any relevant output + there, this method will return it. If the file is empty, an empty string is returned. + + This method will remove previously read output from the returned message. It should + be called after executing every test, to ensure that the message contains only output + relevant for the given test. + + NOTE: running a Godot instance without full access to certain folders in the + home directory (`~/.config`, `~/.local`, `~/.cache`) will result in additional + errors. This happens when GDScript test runner is executed in Docker. However, + these errors do not stop the execution of the program, so they can be ignored. + This method filters them out, leaving only the errors caused by the actual tests. + """ + if self.filename.is_empty(): + return "" + + var error_output = FileAccess.get_file_as_string(self.filename) + error_output = error_output.erase(0, self.already_processed_bytes) + self.already_processed_bytes += len(error_output) + + # By default, Godot's error messages are printed with colors. To include the output in the + # `results.json` file, color markers need to be removed first. + error_output = self.color_sequence.sub(error_output, "", true) + + # Filter out the expected error messages + for expected_error in EXPECTED_GODOT_ERRORS: + error_output = error_output.replace(expected_error, "") + + return error_output + + +class ResultWriter: + + var json_file: String = "" + var stderr: ErrorReader = null + + func _init(json_file: String, stderr: ErrorReader) -> void: + self.json_file = json_file + self.stderr = stderr + + func delete_existing() -> Error: + """ + Removes the previous `results.json` file, to ensure that the current test suite will not use it. + """ + if self.json_file.is_empty(): + return OK + if FileAccess.file_exists(self.json_file): + DirAccess.remove_absolute(self.json_file) + var error_message: String = self.stderr.get_error_message() + if not error_message.is_empty(): + push_error(error_message) + return FAILED + return OK + + func write(results: Dictionary) -> Error: + """ + If output_dir_path is empty, outputs friendly-formatted test results to + stdout. + + Otherwise, writes JSON results to results.json in the given path. + """ + if self.json_file.is_empty(): + return print_friendly_results(results) + else: + return write_results_file(results) + + func print_friendly_results(results: Dictionary) -> Error: + """ + Prints rich results to stdout. + """ + var rich_results = [""] + if results.status == "pass": + rich_results.push_back("[color=yellow]Exercise passed! Nicely done![/color]") + else: + rich_results.push_back("Exercise has at least one failed test:") + for test in results.tests: + if test.status == "pass": + rich_results.push_back(" %s [color=yellow]passed ✔[/color]" % test.name) + else: + rich_results.push_back(" %s [color=pink]failed: %s[/color]" % [test.name, test.message]) + print_rich("\n".join(rich_results)) + return OK + + func write_results_file(results: Dictionary) -> Error: + """ + Saves a dictionary as a `results.json` file in the output directory. The file + is indented with 4 spaces, keys are not sorted. + + The `results` dictionary represents the full output of a single exercise's + test suite, according to the Test Runner Interface documentation: + + https://exercism.org/docs/building/tooling/test-runners/interface + + This method automatically adds `'version' = 2` to the results. + """ + # Due to keys not being sorted, `version` should be inserted first + var full_results = {"version": 2} + full_results.merge(results) + + var pretty_results = JSON.stringify(full_results, " ", false) + + var results_json = FileAccess.open(self.json_file, FileAccess.WRITE) + if results_json == null: + var err = FileAccess.get_open_error() + push_error("Failed to write the file: %s (%s)" % [self.json_file, error_string(err)]) + return err + + results_json.store_string(pretty_results + "\n") + return OK + + +class ArgParser: + """ArgParser reads the cmdline and parses inputs and options.""" + + var solution_script_path: String = "" + var test_suite_script_path: String = "" + var run_all: bool = false + var json: bool = false + var json_file: String = "" + + func usage(message: String) -> void: + print("Usage:") + print("test_runner [--all] [--json filename.json] ") + if not message.is_empty(): + print() + print(message) + + func parse_args() -> Error: + """ + Validates the number of args passed to the script and sets values of the following + global variables, based on the given args: + * `solution_script_path` + * `test_suite_script_path` + """ + var args = OS.get_cmdline_user_args() + var positional: Array = [] + + var idx := 0 + while idx < len(args): + var arg = args[idx] + if arg == "--all": + run_all = true + elif arg.begins_with("--json"): + if arg.contains("="): + var parts = arg.split("=", true, 1) + if parts[0] == "--json": + self.json = true + self.json_file = parts[1] + else: + usage("Invalid flag %s" % arg) + return ERR_INVALID_PARAMETER + elif idx + 1 < len(args): + self.json = true + self.json_file = args[idx + 1] + idx += 1 + else: + usage("Flag %s is missing a filename" % arg) + return ERR_INVALID_PARAMETER + elif arg.begins_with("--"): + usage("Invalid flag %s" % arg) + return ERR_INVALID_PARAMETER + else: + positional.append(arg) + idx += 1 + + if len(positional) != 2: + usage("Expecting 2 positional args but got %d" % len(positional)) + return ERR_INVALID_PARAMETER + + var slug = positional[0] + var solution_dir_path = positional[1] + + # Test folders use dashes, but test files use underscores + var gdscript_path = solution_dir_path.path_join(slug.replace("-", "_")) + + self.solution_script_path = gdscript_path + ".gd" + self.test_suite_script_path = gdscript_path + "_test.gd" + + return OK + + func files() -> Array: + return [self.solution_script_path, self.test_suite_script_path] + + +class Runner: + + var test_suite: Object = null + var solution: Object = null + var stderr: ErrorReader = null + var result_writer: ResultWriter = null + + func _init(test_suite: Object, solution: Object, stderr: ErrorReader, result_writer: ResultWriter) -> void: + self.test_suite = test_suite + self.solution = solution + self.stderr = stderr + self.result_writer = result_writer + + func extract_methods() -> Dictionary: + """Extract the source code of a method from a script.""" + var in_func: bool = false + var parts = [] + var methods: Dictionary = {} + var name: String = "" + var func_re: RegEx = RegEx.create_from_string('^func ([^(]+)\\(') + for line in self.test_suite.get_script().source_code.split("\n"): + if not in_func: + var matched = func_re.search(line) + if matched: + in_func = true + name = matched.get_strings()[1] + parts = [] + elif not line.begins_with("\t") and not line.begins_with(" "): + if in_func: + in_func = false + methods[name] = "\n".join(parts) + else: + parts.append(line) + + return methods + + func get_failed_test_message(actual, expected) -> String: + """ + Generates a message for a failed test. If given values are strings, + this method will wrap them in single quotes, to distinguish from + other types, like integers (e.g. '3' vs 3). + + This method should be used if the actual and expected values for a + given test are different, or if they have different types. + """ + var values = [] + + for value in [actual, expected]: + var formatted_value = str(value) + if typeof(value) == TYPE_STRING: + formatted_value = "'{0}'".format([formatted_value]) + values.append(formatted_value) + + return "Expected output was {1}, actual output was {0}.".format(values) + + func run_tests() -> Array: + """ + Runs the given test suite agains the given solution script. Returns an Array + representing the results of each test case, in the same order that they are defined + in the test suite. Each test result is a Dictionary containing 3 values: + + * `name`: the name of the test case + * `status`: 'pass', 'fail', or 'error' + * `message`: null if the test passed, otherwise it contains the details of the failure/error + + This method will look for methods starting with `test_` in the test script, and + then call them with the solution script as the only argument. The `test_` method's + name will be used as the `name` value for each test result. + + If any stderr output was generated during the execution of the `test_` method, + `status` will be set to 'error'. The test will also receive an 'error' status + if calling the `test_` method returned null (which indicates execution issues). + + Otherwise, the `test_` method should return an Array of 2 elements: the actual value, + and the expected value. The values will be compared and if they are the same, the test will + pass. Otherwise, it will fail, with a relevant message being set by this method. + """ + var test_results = [] + var test_codes = self.extract_methods() + + for method in self.test_suite.get_method_list(): + if not method["name"].begins_with("test_"): + continue + var test_method_name = method["name"] + print("-> " + test_method_name) + var test_method = self.test_suite.get(test_method_name) + + var output = test_method.call(self.solution) + var error_message = self.stderr.get_error_message() + + if output == null: + test_results.append({ + "name": test_method_name, + "status": "error", + "message": error_message, + }) + else: + var actual = output[0] + var expected = output[1] + + var passed = ( + typeof(actual) == typeof(expected) and + actual == expected and + error_message.is_empty() + ) + + var status = "error" + if error_message.is_empty(): + status = "pass" if passed else "fail" + error_message = null if passed else self.get_failed_test_message( + actual, expected + ) + + var test_code = test_codes[method["name"]] + # The code would be cleaner if the test suite actually checked the values. + # Instead, the test suite returns an array with two values which are compared (above). + # This `replace()` hides some of that implementation detail by communicating that the + # array elements are being compared for equality. + test_code = test_code.replace("return [", "assert_equal [") + test_code = test_code.replace("\t", " ") + test_code = test_code.dedent().strip_edges() + + test_results.append({ + "name": test_method_name, + "status": status, + "message": error_message, + "test_code": test_code, + }) + + return test_results + + func run(run_all: bool) -> void: + """ + Executes the current suite against the current solution. Stores the results in the + `results.json` file in the output dir. + """ + var test_results = run_tests() + var results = {} + + # Check if any tests were executed + if len(test_results) == 0: + results = {"status": "error", "message": "No tests were executed.", "tests": []} + else: + results = {"status": "pass", "tests": test_results} + + # If any of the tests failed, change the global status to `fail` + for test_result in test_results: + if test_result["status"] != "pass": + results["status"] = "fail" + break + + if not run_all and results["status"] != "pass": + var success_count: int = 0 + for test_result in test_results: + if test_result["status"] == "pass": + success_count += 1 + + var has_failure = false + var filtered_results = [] + for test_result in test_results: + if test_result["status"] == "pass": + filtered_results.append(test_result) + elif not has_failure: + has_failure = true + test_result["message"] = "Passes %d/%d tests. First failure: %s" % [ + success_count, len(test_results), test_result["message"], + ] + filtered_results.append(test_result) + results["tests"] = filtered_results + + self.result_writer.write(results) + + +func load_script(solution_script_path: String) -> Object: + """Load a script. This helper is needed to catch failures without aborting the error handling.""" + return load(solution_script_path).new() + + +func load_solution_script(solution_script_path: String, result_writer: ResultWriter, stderr: ErrorReader) -> Array: + """ + Loads the solution script and saves it in a global variable. In case of any + issues, this method will save the `results.json` file with `error` status. + """ + # Make sure the STDERR file is empty. + stderr.get_error_message() + var solution_script = load_script(solution_script_path) + + if solution_script == null: + var message: String = "The solution file could not be parsed." + # Extract the error message, up until the "Failed to load" line + # which is specific to the test runner, not the user's solution. + var error_message: String = stderr.get_error_message() + if not error_message.is_empty(): + var msg = [] + var stop = 'ERROR: Failed to load script "%s" with error "Parse error".' % solution_script_path + for line in error_message.split("\n"): + if line == stop: + break + msg.append(line) + message += "\n" + "\n".join(msg) + var results = {"status": "error", "message": message, "tests": []} + result_writer.write(results) + return [null, ERR_PARSE_ERROR] + + return [solution_script, OK] + + +func _init(): + # Calling `quit(1)` doesn't stop the program immediately, so a `return` is necessary. + # That's why errors are checked directly in `_init()`, instead of calling `quit(1)` + # in each method. + var args = ArgParser.new() + if args.parse_args() != OK: + quit(1) + return + for file in args.files(): + if not ResourceLoader.exists(file): + push_error("File %s not found." % file) + quit(1) + return + + var stderr = ErrorReader.new("/tmp/stderr") + if args.json and stderr.check() != OK: + quit(1) + return + + var result_writer = ResultWriter.new(args.json_file, stderr) + if result_writer.delete_existing() != OK: + quit(1) + return + + var r = load_solution_script(args.solution_script_path, result_writer, stderr) + if r[1] != OK: + quit(1) + return + var solution = r[0] + + var test_suite = load(args.test_suite_script_path).new() + + Runner.new(test_suite, solution, stderr, result_writer).run(args.run_all) + quit() diff --git a/exercises/practice/two-bucket/run_tests b/exercises/practice/two-bucket/run_tests new file mode 100755 index 0000000..ed18a90 --- /dev/null +++ b/exercises/practice/two-bucket/run_tests @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +slug=${PWD##*/} +[[ -f "${slug//-/_}.gd" ]] || die "Error! solution file is missing" +[[ -f "${slug//-/_}_test.gd" ]] || die "Error! test file is missing" +exec godot --headless --script ./lib/test_runner.gd -- "${slug}" "${PWD}" diff --git a/exercises/practice/two-bucket/two_bucket.gd b/exercises/practice/two-bucket/two_bucket.gd new file mode 100644 index 0000000..1a609af --- /dev/null +++ b/exercises/practice/two-bucket/two_bucket.gd @@ -0,0 +1,2 @@ +func measure(one: int, two: int, goal: int, start: String): + pass diff --git a/exercises/practice/two-bucket/two_bucket_test.gd b/exercises/practice/two-bucket/two_bucket_test.gd new file mode 100644 index 0000000..3837aff --- /dev/null +++ b/exercises/practice/two-bucket/two_bucket_test.gd @@ -0,0 +1,100 @@ +func test_measure_using_bucket_one_of_size_3_and_bucket_two_of_size_5_start_with_bucket_one(solution_script): + var want = { + "moves": 4, + "goal_bucket": "one", + "other_bucket": 5, + } + var got = solution_script.measure(3, 5, 1, "one") + return [got, want] + + +func test_measure_using_bucket_one_of_size_3_and_bucket_two_of_size_5_start_with_bucket_two(solution_script): + var want = { + "moves": 8, + "goal_bucket": "two", + "other_bucket": 3, + } + var got = solution_script.measure(3, 5, 1, "two") + return [got, want] + + +func test_measure_using_bucket_one_of_size_7_and_bucket_two_of_size_11_start_with_bucket_one(solution_script): + var want = { + "moves": 14, + "goal_bucket": "one", + "other_bucket": 11, + } + var got = solution_script.measure(7, 11, 2, "one") + return [got, want] + + +func test_measure_using_bucket_one_of_size_7_and_bucket_two_of_size_11_start_with_bucket_two(solution_script): + var want = { + "moves": 18, + "goal_bucket": "two", + "other_bucket": 7, + } + var got = solution_script.measure(7, 11, 2, "two") + return [got, want] + + +func test_measure_one_step_using_bucket_one_of_size_1_and_bucket_two_of_size_3_start_with_bucket_two(solution_script): + var want = { + "moves": 1, + "goal_bucket": "two", + "other_bucket": 0, + } + var got = solution_script.measure(1, 3, 3, "two") + return [got, want] + + +func test_measure_using_bucket_one_of_size_2_and_bucket_two_of_size_3_start_with_bucket_one_and_end_with_bucket_two(solution_script): + var want = { + "moves": 2, + "goal_bucket": "two", + "other_bucket": 2, + } + var got = solution_script.measure(2, 3, 3, "one") + return [got, want] + + +func test_measure_using_bucket_one_much_bigger_than_bucket_two(solution_script): + var want = { + "moves": 6, + "goal_bucket": "one", + "other_bucket": 1, + } + var got = solution_script.measure(5, 1, 2, "one") + return [got, want] + + +func test_measure_using_bucket_one_much_smaller_than_bucket_two(solution_script): + var want = { + "moves": 6, + "goal_bucket": "two", + "other_bucket": 0, + } + var got = solution_script.measure(3, 15, 9, "one") + return [got, want] + + +func test_not_possible_to_reach_the_goal(solution_script): + var want = null + var got = solution_script.measure(6, 15, 5, "one") + return [got, want] + + +func test_with_the_same_buckets_but_a_different_goal_then_it_is_possible(solution_script): + var want = { + "moves": 10, + "goal_bucket": "two", + "other_bucket": 0, + } + var got = solution_script.measure(6, 15, 9, "one") + return [got, want] + + +func test_goal_larger_than_both_buckets_is_impossible(solution_script): + var want = null + var got = solution_script.measure(5, 7, 8, "one") + return [got, want]