[INFO] cloning repository https://github.com/jdinkla/ray-tracer-challenge-rust
[INFO] running `Command { std: "git" "-c" "credential.helper=" "-c" "credential.helper=/workspace/cargo-home/bin/git-credential-null" "clone" "--bare" "https://github.com/jdinkla/ray-tracer-challenge-rust" "/workspace/cache/git-repos/https%3A%2F%2Fgithub.com%2Fjdinkla%2Fray-tracer-challenge-rust", kill_on_drop: false }`
[INFO] [stderr] Cloning into bare repository '/workspace/cache/git-repos/https%3A%2F%2Fgithub.com%2Fjdinkla%2Fray-tracer-challenge-rust'...
[INFO] running `Command { std: "git" "rev-parse" "HEAD", kill_on_drop: false }`
[INFO] [stdout] af8984a3c50e191d9c3a1a96543e517278af2ac3
[INFO] testing jdinkla/ray-tracer-challenge-rust against 1.97.0-beta.6 for beta-1.98-1
[INFO] running `Command { std: "git" "clone" "/workspace/cache/git-repos/https%3A%2F%2Fgithub.com%2Fjdinkla%2Fray-tracer-challenge-rust" "/workspace/builds/worker-4-tc1/source", kill_on_drop: false }`
[INFO] [stderr] Cloning into '/workspace/builds/worker-4-tc1/source'...
[INFO] [stderr] done.
[INFO] started tweaking git repo https://github.com/jdinkla/ray-tracer-challenge-rust
[INFO] removed 0 missing tests
[INFO] finished tweaking git repo https://github.com/jdinkla/ray-tracer-challenge-rust
[INFO] tweaked toml for git repo https://github.com/jdinkla/ray-tracer-challenge-rust written to /workspace/builds/worker-4-tc1/source/Cargo.toml
[INFO] validating manifest of git repo https://github.com/jdinkla/ray-tracer-challenge-rust on toolchain 1.97.0-beta.6
[INFO] running `Command { std: CARGO_HOME="/workspace/cargo-home" RUSTUP_HOME="/workspace/rustup-home" "/workspace/cargo-home/bin/cargo" "+1.97.0-beta.6" "metadata" "--manifest-path" "Cargo.toml" "--no-deps", kill_on_drop: false }`
[INFO] crate git repo https://github.com/jdinkla/ray-tracer-challenge-rust already has a lockfile, it will not be regenerated
[INFO] running `Command { std: CARGO_HOME="/workspace/cargo-home" RUSTUP_HOME="/workspace/rustup-home" "/workspace/cargo-home/bin/cargo" "+1.97.0-beta.6" "fetch" "--manifest-path" "Cargo.toml", kill_on_drop: false }`
[INFO] [stderr]     Updating crates.io index
[INFO] [stderr]  Downloading crates ...
[INFO] [stderr]   Downloaded imgref v1.12.2
[INFO] [stderr]   Downloaded arrayvec v0.7.7
[INFO] [stderr]   Downloaded built v0.8.1
[INFO] [stderr]   Downloaded lazy-regex-proc_macros v3.6.0
[INFO] [stderr]   Downloaded globwalk v0.9.1
[INFO] [stderr]   Downloaded typed-builder-macro v0.15.2
[INFO] [stderr]   Downloaded peg v0.6.3
[INFO] [stderr]   Downloaded synthez-codegen v0.3.1
[INFO] [stderr]   Downloaded lazy-regex v3.6.0
[INFO] [stderr]   Downloaded synthez v0.3.1
[INFO] [stderr]   Downloaded inventory v0.3.24
[INFO] [stderr]   Downloaded peg-runtime v0.6.3
[INFO] [stderr]   Downloaded cucumber v0.21.1
[INFO] [stderr]   Downloaded sealed v0.5.0
[INFO] [stderr]   Downloaded smawk v0.3.3
[INFO] [stderr]   Downloaded peg-macros v0.6.3
[INFO] [stderr]   Downloaded cucumber-expressions v0.3.0
[INFO] [stderr]   Downloaded bytecount v0.6.9
[INFO] [stderr]   Downloaded cucumber-codegen v0.21.1
[INFO] [stderr]   Downloaded synthez-core v0.3.1
[INFO] [stderr]   Downloaded gherkin v0.14.0
[INFO] [stderr]   Downloaded typed-builder v0.15.2
[INFO] [stderr]   Downloaded nom_locate v4.2.0
[INFO] [stderr]   Downloaded console v0.15.11
[INFO] [stderr]   Downloaded libfuzzer-sys v0.4.13
[INFO] running `Command { std: "docker" "create" "-v" "/var/lib/crater-agent-workspace/builds/worker-4-tc1/source:/opt/rustwide/workdir:ro,Z" "-v" "/var/lib/crater-agent-workspace/builds/worker-4-tc1/target:/opt/rustwide/target:rw,Z" "-v" "/var/lib/crater-agent-workspace/cargo-home:/opt/rustwide/cargo-home:ro,Z" "-v" "/var/lib/crater-agent-workspace/rustup-home:/opt/rustwide/rustup-home:ro,Z" "-m" "1610612736" "--network" "none" "ghcr.io/rust-lang/crates-build-env/linux@sha256:3d5ced03c013a94a2f102a4510f48a6e9184255caf5fd8244f58017bde7f5210" "sleep" "infinity", kill_on_drop: false }`
[INFO] [stdout] 36ed3a99f224ec607efb46dca28b145504e2bfc7a74cef79729957520fdc650b
[INFO] running `Command { std: "docker" "start" "36ed3a99f224ec607efb46dca28b145504e2bfc7a74cef79729957520fdc650b", kill_on_drop: false }`
[INFO] running `Command { std: "docker" "inspect" "36ed3a99f224ec607efb46dca28b145504e2bfc7a74cef79729957520fdc650b", kill_on_drop: false }`
[INFO] running `Command { std: "docker" "exec" "-e" "SOURCE_DIR=/opt/rustwide/workdir" "-e" "CARGO_HOME=/opt/rustwide/cargo-home" "-e" "RUSTUP_HOME=/opt/rustwide/rustup-home" "-e" "CARGO_TARGET_DIR=/opt/rustwide/target" "-w" "/opt/rustwide/workdir" "--user" "0:0" "36ed3a99f224ec607efb46dca28b145504e2bfc7a74cef79729957520fdc650b" "/opt/rustwide/cargo-home/bin/cargo" "+1.97.0-beta.6" "metadata" "--no-deps" "--format-version=1", kill_on_drop: false }`
[INFO] running `Command { std: "docker" "inspect" "36ed3a99f224ec607efb46dca28b145504e2bfc7a74cef79729957520fdc650b", kill_on_drop: false }`
[INFO] running `Command { std: "docker" "exec" "-e" "SOURCE_DIR=/opt/rustwide/workdir" "-e" "CARGO_HOME=/opt/rustwide/cargo-home" "-e" "RUSTUP_HOME=/opt/rustwide/rustup-home" "-e" "CARGO_TARGET_DIR=/opt/rustwide/target" "-e" "CARGO_INCREMENTAL=0" "-e" "RUST_BACKTRACE=full" "-e" "RUSTFLAGS=--cap-lints=warn" "-e" "RUSTDOCFLAGS=--cap-lints=warn" "-w" "/opt/rustwide/workdir" "--user" "0:0" "36ed3a99f224ec607efb46dca28b145504e2bfc7a74cef79729957520fdc650b" "/opt/rustwide/cargo-home/bin/cargo" "+1.97.0-beta.6" "build" "--frozen" "--message-format=json", kill_on_drop: false }`
[INFO] [stderr]    Compiling autocfg v1.5.1
[INFO] [stderr]    Compiling memchr v2.8.2
[INFO] [stderr]    Compiling either v1.16.0
[INFO] [stderr]    Compiling zerocopy v0.8.52
[INFO] [stderr]    Compiling arrayvec v0.7.7
[INFO] [stderr]    Compiling as-slice v0.2.1
[INFO] [stderr]    Compiling built v0.8.1
[INFO] [stderr]    Compiling log v0.4.33
[INFO] [stderr]    Compiling av-scenechange v0.14.1
[INFO] [stderr]    Compiling syn v2.0.118
[INFO] [stderr]    Compiling y4m v0.8.0
[INFO] [stderr]    Compiling pastey v0.1.1
[INFO] [stderr]    Compiling simd_helpers v0.1.0
[INFO] [stderr]    Compiling imgref v1.12.2
[INFO] [stderr]    Compiling zune-inflate v0.2.54
[INFO] [stderr]    Compiling fax v0.2.7
[INFO] [stderr]    Compiling rgb v0.8.53
[INFO] [stderr]    Compiling aligned v0.4.3
[INFO] [stderr]    Compiling lebe v0.5.3
[INFO] [stderr]    Compiling rayon v1.12.0
[INFO] [stderr]    Compiling itertools v0.14.0
[INFO] [stderr]    Compiling loop9 v0.1.5
[INFO] [stderr]    Compiling bit_field v0.10.3
[INFO] [stderr]    Compiling avif-serialize v0.8.9
[INFO] [stderr]    Compiling bitflags v2.13.0
[INFO] [stderr]    Compiling bytemuck v1.25.0
[INFO] [stderr]    Compiling png v0.18.1
[INFO] [stderr]    Compiling num-traits v0.2.19
[INFO] [stderr]    Compiling nom v8.0.0
[INFO] [stderr]    Compiling no_std_io2 v0.9.4
[INFO] [stderr]    Compiling rav1e v0.8.1
[INFO] [stderr]    Compiling bitstream-io v4.10.0
[INFO] [stderr]    Compiling qoi v0.4.1
[INFO] [stderr]    Compiling num-integer v0.1.46
[INFO] [stderr]    Compiling moxcms v0.8.1
[INFO] [stderr]    Compiling num-bigint v0.4.6
[INFO] [stderr]    Compiling maybe-rayon v0.1.1
[INFO] [stderr]    Compiling num-rational v0.4.2
[INFO] [stderr]    Compiling equator-macro v0.4.2
[INFO] [stderr]    Compiling zerocopy-derive v0.8.52
[INFO] [stderr]    Compiling thiserror-impl v2.0.18
[INFO] [stderr]    Compiling profiling-procmacros v1.0.18
[INFO] [stderr]    Compiling arg_enum_proc_macro v0.3.4
[INFO] [stderr]    Compiling num-derive v0.4.2
[INFO] [stderr]    Compiling profiling v1.0.18
[INFO] [stderr]    Compiling equator v0.4.2
[INFO] [stderr]    Compiling aligned-vec v0.6.4
[INFO] [stderr]    Compiling thiserror v2.0.18
[INFO] [stderr]    Compiling v_frame v0.3.9
[INFO] [stderr]    Compiling av1-grain v0.2.5
[INFO] [stderr]    Compiling half v2.7.1
[INFO] [stderr]    Compiling ravif v0.13.0
[INFO] [stderr]    Compiling tiff v0.11.3
[INFO] [stderr]    Compiling exr v1.74.0
[INFO] [stderr]    Compiling image v0.25.10
[INFO] [stderr]    Compiling ray_tracer_challenge v0.1.0 (/opt/rustwide/workdir)
[INFO] [stderr]     Finished `dev` profile [unoptimized + debuginfo] target(s) in 46.64s
[INFO] running `Command { std: "docker" "inspect" "36ed3a99f224ec607efb46dca28b145504e2bfc7a74cef79729957520fdc650b", kill_on_drop: false }`
[INFO] running `Command { std: "docker" "exec" "-e" "SOURCE_DIR=/opt/rustwide/workdir" "-e" "CARGO_HOME=/opt/rustwide/cargo-home" "-e" "RUSTUP_HOME=/opt/rustwide/rustup-home" "-e" "CARGO_TARGET_DIR=/opt/rustwide/target" "-e" "CARGO_INCREMENTAL=0" "-e" "RUST_BACKTRACE=full" "-e" "RUSTFLAGS=--cap-lints=warn" "-e" "RUSTDOCFLAGS=--cap-lints=warn" "-w" "/opt/rustwide/workdir" "--user" "0:0" "36ed3a99f224ec607efb46dca28b145504e2bfc7a74cef79729957520fdc650b" "/opt/rustwide/cargo-home/bin/cargo" "+1.97.0-beta.6" "test" "--frozen" "--no-run" "--message-format=json", kill_on_drop: false }`
[INFO] [stderr]    Compiling memchr v2.8.2
[INFO] [stderr]    Compiling either v1.16.0
[INFO] [stderr]    Compiling serde_core v1.0.228
[INFO] [stderr]    Compiling zmij v1.0.21
[INFO] [stderr]    Compiling rustix v1.1.4
[INFO] [stderr]    Compiling bytecount v0.6.9
[INFO] [stderr]    Compiling equator-macro v0.4.2
[INFO] [stderr]    Compiling zerocopy-derive v0.8.52
[INFO] [stderr]    Compiling thiserror-impl v2.0.18
[INFO] [stderr]    Compiling serde v1.0.228
[INFO] [stderr]    Compiling arg_enum_proc_macro v0.3.4
[INFO] [stderr]    Compiling serde_json v1.0.150
[INFO] [stderr]    Compiling profiling-procmacros v1.0.18
[INFO] [stderr]    Compiling sealed v0.5.0
[INFO] [stderr]    Compiling futures-core v0.3.32
[INFO] [stderr]    Compiling itoa v1.0.18
[INFO] [stderr]    Compiling futures-sink v0.3.32
[INFO] [stderr]    Compiling num-derive v0.4.2
[INFO] [stderr]    Compiling futures-macro v0.3.32
[INFO] [stderr]    Compiling futures-channel v0.3.32
[INFO] [stderr]    Compiling rayon v1.12.0
[INFO] [stderr]    Compiling itertools v0.14.0
[INFO] [stderr]    Compiling derive_more v0.99.20
[INFO] [stderr]    Compiling profiling v1.0.18
[INFO] [stderr]    Compiling slab v0.4.12
[INFO] [stderr]    Compiling aho-corasick v1.1.4
[INFO] [stderr]    Compiling no_std_io2 v0.9.4
[INFO] [stderr]    Compiling nom v8.0.0
[INFO] [stderr]    Compiling bitstream-io v4.10.0
[INFO] [stderr]    Compiling bstr v1.12.1
[INFO] [stderr]    Compiling equator v0.4.2
[INFO] [stderr]    Compiling aligned-vec v0.6.4
[INFO] [stderr]    Compiling synthez-core v0.3.1
[INFO] [stderr]    Compiling nom v7.1.3
[INFO] [stderr]    Compiling v_frame v0.3.9
[INFO] [stderr]    Compiling futures-io v0.3.32
[INFO] [stderr]    Compiling regex-syntax v0.7.5
[INFO] [stderr]    Compiling peg-runtime v0.6.3
[INFO] [stderr]    Compiling thiserror v2.0.18
[INFO] [stderr]    Compiling futures-util v0.3.32
[INFO] [stderr]    Compiling terminal_size v0.4.4
[INFO] [stderr]    Compiling typed-builder-macro v0.15.2
[INFO] [stderr]    Compiling thiserror-impl v1.0.69
[INFO] [stderr]    Compiling peg-macros v0.6.3
[INFO] [stderr]    Compiling zerocopy v0.8.52
[INFO] [stderr]    Compiling synthez-codegen v0.3.1
[INFO] [stderr]    Compiling smawk v0.3.3
[INFO] [stderr]    Compiling regex-automata v0.4.14
[INFO] [stderr]    Compiling textwrap v0.16.2
[INFO] [stderr]    Compiling typed-builder v0.15.2
[INFO] [stderr]    Compiling clap_builder v4.6.0
[INFO] [stderr]    Compiling peg v0.6.3
[INFO] [stderr]    Compiling clap_derive v4.6.1
[INFO] [stderr]    Compiling thiserror v1.0.69
[INFO] [stderr]    Compiling png v0.18.1
[INFO] [stderr]    Compiling nom_locate v4.2.0
[INFO] [stderr]    Compiling synthez v0.3.1
[INFO] [stderr]    Compiling itertools v0.13.0
[INFO] [stderr]    Compiling pin-project-internal v1.1.13
[INFO] [stderr]    Compiling av-scenechange v0.14.1
[INFO] [stderr]    Compiling maybe-rayon v0.1.1
[INFO] [stderr]    Compiling inflections v1.1.1
[INFO] [stderr]    Compiling console v0.15.11
[INFO] [stderr]    Compiling smart-default v0.7.1
[INFO] [stderr]    Compiling gherkin v0.14.0
[INFO] [stderr]    Compiling linked-hash-map v0.5.6
[INFO] [stderr]    Compiling inventory v0.3.24
[INFO] [stderr]    Compiling humantime v2.3.0
[INFO] [stderr]    Compiling drain_filter_polyfill v0.1.3
[INFO] [stderr]    Compiling futures-executor v0.3.32
[INFO] [stderr]    Compiling pin-project v1.1.13
[INFO] [stderr]    Compiling av1-grain v0.2.5
[INFO] [stderr]    Compiling futures v0.3.32
[INFO] [stderr]    Compiling regex v1.12.4
[INFO] [stderr]    Compiling globset v0.4.18
[INFO] [stderr]    Compiling clap v4.6.1
[INFO] [stderr]    Compiling rav1e v0.8.1
[INFO] [stderr]    Compiling cucumber-expressions v0.3.0
[INFO] [stderr]    Compiling ignore v0.4.26
[INFO] [stderr]    Compiling lazy-regex-proc_macros v3.6.0
[INFO] [stderr]    Compiling half v2.7.1
[INFO] [stderr]    Compiling globwalk v0.9.1
[INFO] [stderr]    Compiling cucumber-codegen v0.21.1
[INFO] [stderr]    Compiling lazy-regex v3.6.0
[INFO] [stderr]    Compiling tiff v0.11.3
[INFO] [stderr]    Compiling exr v1.74.0
[INFO] [stderr]    Compiling cucumber v0.21.1
[INFO] [stderr]    Compiling ravif v0.13.0
[INFO] [stderr]    Compiling image v0.25.10
[INFO] [stderr]    Compiling ray_tracer_challenge v0.1.0 (/opt/rustwide/workdir)
[INFO] [stderr]     Finished `test` profile [unoptimized + debuginfo] target(s) in 1m 41s
[INFO] running `Command { std: "docker" "inspect" "36ed3a99f224ec607efb46dca28b145504e2bfc7a74cef79729957520fdc650b", kill_on_drop: false }`
[INFO] running `Command { std: "docker" "exec" "-e" "SOURCE_DIR=/opt/rustwide/workdir" "-e" "CARGO_HOME=/opt/rustwide/cargo-home" "-e" "RUSTUP_HOME=/opt/rustwide/rustup-home" "-e" "CARGO_TARGET_DIR=/opt/rustwide/target" "-e" "CARGO_INCREMENTAL=0" "-e" "RUST_BACKTRACE=full" "-e" "RUSTFLAGS=--cap-lints=warn" "-e" "RUSTDOCFLAGS=--cap-lints=warn" "-w" "/opt/rustwide/workdir" "--user" "0:0" "36ed3a99f224ec607efb46dca28b145504e2bfc7a74cef79729957520fdc650b" "/opt/rustwide/cargo-home/bin/cargo" "+1.97.0-beta.6" "test" "--frozen", kill_on_drop: false }`
[INFO] [stderr]     Finished `test` profile [unoptimized + debuginfo] target(s) in 0.41s
[INFO] [stderr]      Running unittests src/lib.rs (/opt/rustwide/target/debug/deps/ray_tracer_challenge-7f216a471278ffe4)
[INFO] [stdout] 
[INFO] [stdout] running 6 tests
[INFO] [stdout] test math::tuple::tests::cross_product_is_anticommutative ... ok
[INFO] [stdout] test world::tests::an_empty_world_has_no_objects ... ok
[INFO] [stdout] test world::tests::color_at_a_ray_that_hits ... ok
[INFO] [stdout] test world::tests::color_at_a_ray_that_misses_is_black ... ok
[INFO] [stdout] test math::tuple::tests::magnitude_and_normalize ... ok
[INFO] [stdout] test math::tuple::tests::point_and_vector_have_expected_w ... ok
[INFO] [stdout] 
[INFO] [stdout] test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s
[INFO] [stdout] 
[INFO] [stderr]      Running tests/cucumber.rs (/opt/rustwide/target/debug/deps/cucumber-617287fb686f9333)
[INFO] [stdout] Feature: Camera
[INFO] [stdout]   Scenario: Constructing a camera
[INFO] [stdout]    ✔  Given hsize ← 160
[INFO] [stdout]    ✔  And vsize ← 120
[INFO] [stdout]    ✔  And field_of_view ← 1.57079632679
[INFO] [stdout]    ✔  When c ← camera(hsize, vsize, field_of_view)
[INFO] [stdout]    ✔  Then c.hsize = 160
[INFO] [stdout]    ✔  And c.vsize = 120
[INFO] [stdout]    ✔  And c.field_of_view = 1.57079632679
[INFO] [stdout]    ✔  And c.transform = identity_matrix
[INFO] [stdout]   Scenario: The pixel size for a horizontal canvas
[INFO] [stdout]    ✔  Given c ← camera(200, 125, 1.57079632679)
[INFO] [stdout]    ✔  Then c.pixel_size = 0.01
[INFO] [stdout]   Scenario: The pixel size for a vertical canvas
[INFO] [stdout]    ✔  Given c ← camera(125, 200, 1.57079632679)
[INFO] [stdout]    ✔  Then c.pixel_size = 0.01
[INFO] [stdout]   Scenario: Constructing a ray through the center of the canvas
[INFO] [stdout]    ✔  Given c ← camera(201, 101, 1.57079632679)
[INFO] [stdout]    ✔  When r ← ray_for_pixel(c, 100, 50)
[INFO] [stdout]    ✔  Then r.origin = point(0, 0, 0)
[INFO] [stdout]    ✔  And r.direction = vector(0, 0, -1)
[INFO] [stdout]   Scenario: Constructing a ray through a corner of the canvas
[INFO] [stdout]    ✔  Given c ← camera(201, 101, 1.57079632679)
[INFO] [stdout]    ✔  When r ← ray_for_pixel(c, 0, 0)
[INFO] [stdout]    ✔  Then r.origin = point(0, 0, 0)
[INFO] [stdout]    ✔  And r.direction = vector(0.66519, 0.33259, -0.66851)
[INFO] [stdout]   Scenario: Constructing a ray when the camera is transformed
[INFO] [stdout]    ✔  Given c ← camera(201, 101, 1.57079632679)
[INFO] [stdout]    ✔  When c.transform ← rotation_y(0.78539816339) * translation(0, -2, 5)
[INFO] [stdout]    ✔  And r ← ray_for_pixel(c, 100, 50)
[INFO] [stdout]    ✔  Then r.origin = point(0, 2, -5)
[INFO] [stdout]    ✔  And r.direction = vector(0.70710678118, 0.0, -0.70710678118)
[INFO] [stdout]   Scenario: Rendering a world with a camera
[INFO] [stdout]    ✔  Given w ← defaultWorld
[INFO] [stdout]    ✔  And c ← camera(11, 11, 1.57079632679)
[INFO] [stdout]    ✔  And from ← point(0, 0, -5)
[INFO] [stdout]    ✔  And to ← point(0, 0, 0)
[INFO] [stdout]    ✔  And up ← vector(0, 1, 0)
[INFO] [stdout]    ✔  And c.transform ← view_transform(from, to, up)
[INFO] [stdout]    ✔  When image ← render(c, w)
[INFO] [stdout]    ✔  Then pixel_at(image, 5, 5) = color(0.38066, 0.47583, 0.2855)
[INFO] [stdout] Feature: Canvas
[INFO] [stdout]   Scenario: Creating a canvas
[INFO] [stdout]    ✔  Given c ← canvas(10, 20)
[INFO] [stdout]    ✔  Then c.width = 10
[INFO] [stdout]    ✔  And c.height = 20
[INFO] [stdout]    ✔  And every pixel of c is color(0, 0, 0)
[INFO] [stdout]   Scenario: Writing pixels to a canvas
[INFO] [stdout]    ✔  Given c ← canvas(10, 20)
[INFO] [stdout]    ✔  And red ← color(1, 0, 0)
[INFO] [stdout]    ✔  When write_pixel(c, 2, 3, red)
[INFO] [stdout]    ✔  Then pixel_at(c, 2, 3) = red
[INFO] [stdout]   Scenario: Constructing the PPM header
[INFO] [stdout]    ✔  Given c ← canvas(5, 3)
[INFO] [stdout]    ✔  When ppm ← canvas_to_ppm(c)
[INFO] [stdout]    ✔  Then lines 1-3 of ppm are
[INFO] [stdout]   Scenario: Constructing the PPM pixel data
[INFO] [stdout]    ✔  Given c ← canvas(5, 3)
[INFO] [stdout]    ✔  And c1 ← color(1.5, 0, 0)
[INFO] [stdout]    ✔  And c2 ← color(0, 0.5, 0)
[INFO] [stdout]    ✔  And c3 ← color(-0.5, 0, 1)
[INFO] [stdout]    ✔  When write_pixel(c, 0, 0, c1)
[INFO] [stdout]    ✔  And write_pixel(c, 2, 1, c2)
[INFO] [stdout]    ✔  And write_pixel(c, 4, 2, c3)
[INFO] [stdout]    ✔  And ppm ← canvas_to_ppm(c)
[INFO] [stdout]    ✔  Then lines 4-6 of ppm are
[INFO] [stdout]   Scenario: Splitting long lines in PPM files
[INFO] [stdout]    ✔  Given c ← canvas(10, 2)
[INFO] [stdout]    ✔  When every pixel of c is set to color(1.0, 0.8, 0.6)
[INFO] [stdout]    ✔  And ppm ← canvas_to_ppm(c)
[INFO] [stdout]    ✔  Then lines 4-7 of ppm are
[INFO] [stdout]   Scenario: PPM files are terminated by a newline character
[INFO] [stdout]    ✔  Given c ← canvas(5, 3)
[INFO] [stdout]    ✔  When ppm ← canvas_to_ppm(c)
[INFO] [stdout]    ✔  Then ppm ends with a newline character
[INFO] [stdout] Feature: Cones
[INFO] [stdout]   Scenario Outline: Intersecting a cone with a ray
[INFO] [stdout]    ✔  Given shape ← cone()
[INFO] [stdout]    ✔  And direction ← normalize(vector(0, 0, 1))
[INFO] [stdout]    ✔  And r ← ray(point(0, 0, -5), direction)
[INFO] [stdout]    ✔  When xs ← local_intersect(shape, r)
[INFO] [stdout]    ✔  Then xs.count = 2
[INFO] [stdout]    ✔  And xs[0].t = 5
[INFO] [stdout]    ✔  And xs[1].t = 5
[INFO] [stdout]   Scenario Outline: Intersecting a cone with a ray
[INFO] [stdout]    ✔  Given shape ← cone()
[INFO] [stdout]    ✔  And direction ← normalize(vector(1, 1, 1))
[INFO] [stdout]    ✔  And r ← ray(point(0, 0, -5), direction)
[INFO] [stdout]    ✔  When xs ← local_intersect(shape, r)
[INFO] [stdout]    ✔  Then xs.count = 2
[INFO] [stdout]    ✔  And xs[0].t = 8.66025
[INFO] [stdout]    ✔  And xs[1].t = 8.66025
[INFO] [stdout]   Scenario Outline: Intersecting a cone with a ray
[INFO] [stdout]    ✔  Given shape ← cone()
[INFO] [stdout]    ✔  And direction ← normalize(vector(-0.5, -1, 1))
[INFO] [stdout]    ✔  And r ← ray(point(1, 1, -5), direction)
[INFO] [stdout]    ✔  When xs ← local_intersect(shape, r)
[INFO] [stdout]    ✔  Then xs.count = 2
[INFO] [stdout]    ✔  And xs[0].t = 4.55006
[INFO] [stdout]    ✔  And xs[1].t = 49.44994
[INFO] [stdout]   Scenario: Intersecting a cone with a ray parallel to one of its halves
[INFO] [stdout]    ✔  Given shape ← cone()
[INFO] [stdout]    ✔  And direction ← normalize(vector(0, 1, 1))
[INFO] [stdout]    ✔  And r ← ray(point(0, 0, -1), direction)
[INFO] [stdout]    ✔  When xs ← local_intersect(shape, r)
[INFO] [stdout]    ✔  Then xs.count = 1
[INFO] [stdout]    ✔  And xs[0].t = 0.35355
[INFO] [stdout]   Scenario Outline: Intersecting a cone's end caps
[INFO] [stdout]    ✔  Given shape ← cone()
[INFO] [stdout]    ✔  And shape.minimum ← -0.5
[INFO] [stdout]    ✔  And shape.maximum ← 0.5
[INFO] [stdout]    ✔  And shape.closed ← true
[INFO] [stdout]    ✔  And direction ← normalize(vector(0, 1, 0))
[INFO] [stdout]    ✔  And r ← ray(point(0, 0, -5), direction)
[INFO] [stdout]    ✔  When xs ← local_intersect(shape, r)
[INFO] [stdout]    ✔  Then xs.count = 0
[INFO] [stdout]   Scenario Outline: Intersecting a cone's end caps
[INFO] [stdout]    ✔  Given shape ← cone()
[INFO] [stdout]    ✔  And shape.minimum ← -0.5
[INFO] [stdout]    ✔  And shape.maximum ← 0.5
[INFO] [stdout]    ✔  And shape.closed ← true
[INFO] [stdout]    ✔  And direction ← normalize(vector(0, 1, 1))
[INFO] [stdout]    ✔  And r ← ray(point(0, 0, -0.25), direction)
[INFO] [stdout]    ✔  When xs ← local_intersect(shape, r)
[INFO] [stdout]    ✔  Then xs.count = 2
[INFO] [stdout]   Scenario Outline: Intersecting a cone's end caps
[INFO] [stdout]    ✔  Given shape ← cone()
[INFO] [stdout]    ✔  And shape.minimum ← -0.5
[INFO] [stdout]    ✔  And shape.maximum ← 0.5
[INFO] [stdout]    ✔  And shape.closed ← true
[INFO] [stdout]    ✔  And direction ← normalize(vector(0, 1, 0))
[INFO] [stdout]    ✔  And r ← ray(point(0, 0, -0.25), direction)
[INFO] [stdout]    ✔  When xs ← local_intersect(shape, r)
[INFO] [stdout]    ✔  Then xs.count = 4
[INFO] [stdout]   Scenario Outline: Computing the normal vector on a cone
[INFO] [stdout]    ✔  Given shape ← cone()
[INFO] [stdout]    ✔  When n ← local_normal_at(shape, point(0, 0, 0))
[INFO] [stdout]    ✔  Then n = vector(0, 0, 0)
[INFO] [stdout]   Scenario Outline: Computing the normal vector on a cone
[INFO] [stdout]    ✔  Given shape ← cone()
[INFO] [stdout]    ✔  When n ← local_normal_at(shape, point(1, 1, 1))
[INFO] [stdout]    ✔  Then n = vector(1, -1.41421356237, 1)
[INFO] [stdout]   Scenario Outline: Computing the normal vector on a cone
[INFO] [stdout]    ✔  Given shape ← cone()
[INFO] [stdout]    ✔  When n ← local_normal_at(shape, point(-1, -1, 0))
[INFO] [stdout]    ✔  Then n = vector(-1, 1, 0)
[INFO] [stdout] Feature: Constructive Solid Geometry (CSG)
[INFO] [stdout]   Scenario: CSG is created with an operation and two shapes
[INFO] [stdout]    ✔  Given s1 ← sphere()
[INFO] [stdout]    ✔  And s2 ← cube()
[INFO] [stdout]    ✔  When c ← csg("union", s1, s2)
[INFO] [stdout]    ✔  Then c.operation = "union"
[INFO] [stdout]    ✔  And c.left = s1
[INFO] [stdout]    ✔  And c.right = s2
[INFO] [stdout]    ✔  And s1.parent = c
[INFO] [stdout]    ✔  And s2.parent = c
[INFO] [stdout]   Scenario Outline: Evaluating the rule for a CSG operation
[INFO] [stdout]    ✔  When result ← intersection_allowed("union", true, true, true)
[INFO] [stdout]    ✔  Then result = false
[INFO] [stdout]   Scenario Outline: Evaluating the rule for a CSG operation
[INFO] [stdout]    ✔  When result ← intersection_allowed("union", true, true, false)
[INFO] [stdout]    ✔  Then result = true
[INFO] [stdout]   Scenario Outline: Evaluating the rule for a CSG operation
[INFO] [stdout]    ✔  When result ← intersection_allowed("union", true, false, true)
[INFO] [stdout]    ✔  Then result = false
[INFO] [stdout]   Scenario Outline: Evaluating the rule for a CSG operation
[INFO] [stdout]    ✔  When result ← intersection_allowed("union", true, false, false)
[INFO] [stdout]    ✔  Then result = true
[INFO] [stdout]   Scenario Outline: Evaluating the rule for a CSG operation
[INFO] [stdout]    ✔  When result ← intersection_allowed("union", false, true, true)
[INFO] [stdout]    ✔  Then result = false
[INFO] [stdout]   Scenario Outline: Evaluating the rule for a CSG operation
[INFO] [stdout]    ✔  When result ← intersection_allowed("union", false, true, false)
[INFO] [stdout]    ✔  Then result = false
[INFO] [stdout]   Scenario Outline: Evaluating the rule for a CSG operation
[INFO] [stdout]    ✔  When result ← intersection_allowed("union", false, false, true)
[INFO] [stdout]    ✔  Then result = true
[INFO] [stdout]   Scenario Outline: Evaluating the rule for a CSG operation
[INFO] [stdout]    ✔  When result ← intersection_allowed("union", false, false, false)
[INFO] [stdout]    ✔  Then result = true
[INFO] [stdout]   Scenario Outline: Evaluating the rule for a CSG operation
[INFO] [stdout]    ✔  When result ← intersection_allowed("intersection", true, true, true)
[INFO] [stdout]    ✔  Then result = true
[INFO] [stdout]   Scenario Outline: Evaluating the rule for a CSG operation
[INFO] [stdout]    ✔  When result ← intersection_allowed("intersection", true, true, false)
[INFO] [stdout]    ✔  Then result = false
[INFO] [stdout]   Scenario Outline: Evaluating the rule for a CSG operation
[INFO] [stdout]    ✔  When result ← intersection_allowed("intersection", true, false, true)
[INFO] [stdout]    ✔  Then result = true
[INFO] [stdout]   Scenario Outline: Evaluating the rule for a CSG operation
[INFO] [stdout]    ✔  When result ← intersection_allowed("intersection", true, false, false)
[INFO] [stdout]    ✔  Then result = false
[INFO] [stdout]   Scenario Outline: Evaluating the rule for a CSG operation
[INFO] [stdout]    ✔  When result ← intersection_allowed("intersection", false, true, true)
[INFO] [stdout]    ✔  Then result = true
[INFO] [stdout]   Scenario Outline: Evaluating the rule for a CSG operation
[INFO] [stdout]    ✔  When result ← intersection_allowed("intersection", false, true, false)
[INFO] [stdout]    ✔  Then result = true
[INFO] [stdout]   Scenario Outline: Evaluating the rule for a CSG operation
[INFO] [stdout]    ✔  When result ← intersection_allowed("intersection", false, false, true)
[INFO] [stdout]    ✔  Then result = false
[INFO] [stdout]   Scenario Outline: Evaluating the rule for a CSG operation
[INFO] [stdout]    ✔  When result ← intersection_allowed("intersection", false, false, false)
[INFO] [stdout]    ✔  Then result = false
[INFO] [stdout]   Scenario Outline: Evaluating the rule for a CSG operation
[INFO] [stdout]    ✔  When result ← intersection_allowed("difference", true, true, true)
[INFO] [stdout]    ✔  Then result = false
[INFO] [stdout]   Scenario Outline: Evaluating the rule for a CSG operation
[INFO] [stdout]    ✔  When result ← intersection_allowed("difference", true, true, false)
[INFO] [stdout]    ✔  Then result = true
[INFO] [stdout]   Scenario Outline: Evaluating the rule for a CSG operation
[INFO] [stdout]    ✔  When result ← intersection_allowed("difference", true, false, true)
[INFO] [stdout]    ✔  Then result = false
[INFO] [stdout]   Scenario Outline: Evaluating the rule for a CSG operation
[INFO] [stdout]    ✔  When result ← intersection_allowed("difference", true, false, false)
[INFO] [stdout]    ✔  Then result = true
[INFO] [stdout]   Scenario Outline: Evaluating the rule for a CSG operation
[INFO] [stdout]    ✔  When result ← intersection_allowed("difference", false, true, true)
[INFO] [stdout]    ✔  Then result = true
[INFO] [stdout]   Scenario Outline: Evaluating the rule for a CSG operation
[INFO] [stdout]    ✔  When result ← intersection_allowed("difference", false, true, false)
[INFO] [stdout]    ✔  Then result = true
[INFO] [stdout]   Scenario Outline: Evaluating the rule for a CSG operation
[INFO] [stdout]    ✔  When result ← intersection_allowed("difference", false, false, true)
[INFO] [stdout]    ✔  Then result = false
[INFO] [stdout]   Scenario Outline: Evaluating the rule for a CSG operation
[INFO] [stdout]    ✔  When result ← intersection_allowed("difference", false, false, false)
[INFO] [stdout]    ✔  Then result = false
[INFO] [stdout]   Scenario Outline: Filtering a list of intersections
[INFO] [stdout]    ✔  Given s1 ← sphere()
[INFO] [stdout]    ✔  And s2 ← cube()
[INFO] [stdout]    ✔  And c ← csg("union", s1, s2)
[INFO] [stdout]    ✔  And xs ← intersections(1:s1, 2:s2, 3:s1, 4:s2)
[INFO] [stdout]    ✔  When result ← filter_intersections(c, xs)
[INFO] [stdout]    ✔  Then result.count = 2
[INFO] [stdout]    ✔  And result[0] = xs[0]
[INFO] [stdout]    ✔  And result[1] = xs[3]
[INFO] [stdout]   Scenario Outline: Filtering a list of intersections
[INFO] [stdout]    ✔  Given s1 ← sphere()
[INFO] [stdout]    ✔  And s2 ← cube()
[INFO] [stdout]    ✔  And c ← csg("intersection", s1, s2)
[INFO] [stdout]    ✔  And xs ← intersections(1:s1, 2:s2, 3:s1, 4:s2)
[INFO] [stdout]    ✔  When result ← filter_intersections(c, xs)
[INFO] [stdout]    ✔  Then result.count = 2
[INFO] [stdout]    ✔  And result[0] = xs[1]
[INFO] [stdout]    ✔  And result[1] = xs[2]
[INFO] [stdout]   Scenario Outline: Filtering a list of intersections
[INFO] [stdout]    ✔  Given s1 ← sphere()
[INFO] [stdout]    ✔  And s2 ← cube()
[INFO] [stdout]    ✔  And c ← csg("difference", s1, s2)
[INFO] [stdout]    ✔  And xs ← intersections(1:s1, 2:s2, 3:s1, 4:s2)
[INFO] [stdout]    ✔  When result ← filter_intersections(c, xs)
[INFO] [stdout]    ✔  Then result.count = 2
[INFO] [stdout]    ✔  And result[0] = xs[0]
[INFO] [stdout]    ✔  And result[1] = xs[1]
[INFO] [stdout]   Scenario: A ray misses a CSG object
[INFO] [stdout]    ✔  Given c ← csg("union", sphere(), cube())
[INFO] [stdout]    ✔  And r ← ray(point(0, 2, -5), vector(0, 0, 1))
[INFO] [stdout]    ✔  When xs ← local_intersect(c, r)
[INFO] [stdout]    ✔  Then xs is empty
[INFO] [stdout]   Scenario: A ray hits a CSG object
[INFO] [stdout]    ✔  Given s1 ← sphere()
[INFO] [stdout]    ✔  And s2 ← sphere()
[INFO] [stdout]    ✔  And set_transform(s2, translation(0, 0, 0.5))
[INFO] [stdout]    ✔  And c ← csg("union", s1, s2)
[INFO] [stdout]    ✔  And r ← ray(point(0, 0, -5), vector(0, 0, 1))
[INFO] [stdout]    ✔  When xs ← local_intersect(c, r)
[INFO] [stdout]    ✔  Then xs.count = 2
[INFO] [stdout]    ✔  And xs[0].t = 4
[INFO] [stdout]    ✔  And xs[0].object = s1
[INFO] [stdout]    ✔  And xs[1].t = 6.5
[INFO] [stdout]    ✔  And xs[1].object = s2
[INFO] [stdout] Feature: Cubes
[INFO] [stdout]   Scenario Outline: A ray intersects a cube
[INFO] [stdout]    ✔  Given c ← cube()
[INFO] [stdout]    ✔  And r ← ray(point(5, 0.5, 0), vector(-1, 0, 0))
[INFO] [stdout]    ✔  When xs ← local_intersect(c, r)
[INFO] [stdout]    ✔  Then xs.count = 2
[INFO] [stdout]    ✔  And xs[0].t = 4
[INFO] [stdout]    ✔  And xs[1].t = 6
[INFO] [stdout]   Scenario Outline: A ray intersects a cube
[INFO] [stdout]    ✔  Given c ← cube()
[INFO] [stdout]    ✔  And r ← ray(point(-5, 0.5, 0), vector(1, 0, 0))
[INFO] [stdout]    ✔  When xs ← local_intersect(c, r)
[INFO] [stdout]    ✔  Then xs.count = 2
[INFO] [stdout]    ✔  And xs[0].t = 4
[INFO] [stdout]    ✔  And xs[1].t = 6
[INFO] [stdout]   Scenario Outline: A ray intersects a cube
[INFO] [stdout]    ✔  Given c ← cube()
[INFO] [stdout]    ✔  And r ← ray(point(0.5, 5, 0), vector(0, -1, 0))
[INFO] [stdout]    ✔  When xs ← local_intersect(c, r)
[INFO] [stdout]    ✔  Then xs.count = 2
[INFO] [stdout]    ✔  And xs[0].t = 4
[INFO] [stdout]    ✔  And xs[1].t = 6
[INFO] [stdout]   Scenario Outline: A ray intersects a cube
[INFO] [stdout]    ✔  Given c ← cube()
[INFO] [stdout]    ✔  And r ← ray(point(0.5, -5, 0), vector(0, 1, 0))
[INFO] [stdout]    ✔  When xs ← local_intersect(c, r)
[INFO] [stdout]    ✔  Then xs.count = 2
[INFO] [stdout]    ✔  And xs[0].t = 4
[INFO] [stdout]    ✔  And xs[1].t = 6
[INFO] [stdout]   Scenario Outline: A ray intersects a cube
[INFO] [stdout]    ✔  Given c ← cube()
[INFO] [stdout]    ✔  And r ← ray(point(0.5, 0, 5), vector(0, 0, -1))
[INFO] [stdout]    ✔  When xs ← local_intersect(c, r)
[INFO] [stdout]    ✔  Then xs.count = 2
[INFO] [stdout]    ✔  And xs[0].t = 4
[INFO] [stdout]    ✔  And xs[1].t = 6
[INFO] [stdout]   Scenario Outline: A ray intersects a cube
[INFO] [stdout]    ✔  Given c ← cube()
[INFO] [stdout]    ✔  And r ← ray(point(0.5, 0, -5), vector(0, 0, 1))
[INFO] [stdout]    ✔  When xs ← local_intersect(c, r)
[INFO] [stdout]    ✔  Then xs.count = 2
[INFO] [stdout]    ✔  And xs[0].t = 4
[INFO] [stdout]    ✔  And xs[1].t = 6
[INFO] [stdout]   Scenario Outline: A ray intersects a cube
[INFO] [stdout]    ✔  Given c ← cube()
[INFO] [stdout]    ✔  And r ← ray(point(0, 0.5, 0), vector(0, 0, 1))
[INFO] [stdout]    ✔  When xs ← local_intersect(c, r)
[INFO] [stdout]    ✔  Then xs.count = 2
[INFO] [stdout]    ✔  And xs[0].t = -1
[INFO] [stdout]    ✔  And xs[1].t = 1
[INFO] [stdout]   Scenario Outline: A ray misses a cube
[INFO] [stdout]    ✔  Given c ← cube()
[INFO] [stdout]    ✔  And r ← ray(point(-2, 0, 0), vector(0.2673, 0.5345, 0.8018))
[INFO] [stdout]    ✔  When xs ← local_intersect(c, r)
[INFO] [stdout]    ✔  Then xs.count = 0
[INFO] [stdout]   Scenario Outline: A ray misses a cube
[INFO] [stdout]    ✔  Given c ← cube()
[INFO] [stdout]    ✔  And r ← ray(point(0, -2, 0), vector(0.8018, 0.2673, 0.5345))
[INFO] [stdout]    ✔  When xs ← local_intersect(c, r)
[INFO] [stdout]    ✔  Then xs.count = 0
[INFO] [stdout]   Scenario Outline: A ray misses a cube
[INFO] [stdout]    ✔  Given c ← cube()
[INFO] [stdout]    ✔  And r ← ray(point(0, 0, -2), vector(0.5345, 0.8018, 0.2673))
[INFO] [stdout]    ✔  When xs ← local_intersect(c, r)
[INFO] [stdout]    ✔  Then xs.count = 0
[INFO] [stdout]   Scenario Outline: A ray misses a cube
[INFO] [stdout]    ✔  Given c ← cube()
[INFO] [stdout]    ✔  And r ← ray(point(2, 0, 2), vector(0, 0, -1))
[INFO] [stdout]    ✔  When xs ← local_intersect(c, r)
[INFO] [stdout]    ✔  Then xs.count = 0
[INFO] [stdout]   Scenario Outline: A ray misses a cube
[INFO] [stdout]    ✔  Given c ← cube()
[INFO] [stdout]    ✔  And r ← ray(point(0, 2, 2), vector(0, -1, 0))
[INFO] [stdout]    ✔  When xs ← local_intersect(c, r)
[INFO] [stdout]    ✔  Then xs.count = 0
[INFO] [stdout]   Scenario Outline: A ray misses a cube
[INFO] [stdout]    ✔  Given c ← cube()
[INFO] [stdout]    ✔  And r ← ray(point(2, 2, 0), vector(-1, 0, 0))
[INFO] [stdout]    ✔  When xs ← local_intersect(c, r)
[INFO] [stdout]    ✔  Then xs.count = 0
[INFO] [stdout]   Scenario Outline: The normal on the surface of a cube
[INFO] [stdout]    ✔  Given c ← cube()
[INFO] [stdout]    ✔  And p ← point(1, 0.5, -0.8)
[INFO] [stdout]    ✔  When normal ← local_normal_at(c, p)
[INFO] [stdout]    ✔  Then normal = vector(1, 0, 0)
[INFO] [stdout]   Scenario Outline: The normal on the surface of a cube
[INFO] [stdout]    ✔  Given c ← cube()
[INFO] [stdout]    ✔  And p ← point(-1, -0.2, 0.9)
[INFO] [stdout]    ✔  When normal ← local_normal_at(c, p)
[INFO] [stdout]    ✔  Then normal = vector(-1, 0, 0)
[INFO] [stdout]   Scenario Outline: The normal on the surface of a cube
[INFO] [stdout]    ✔  Given c ← cube()
[INFO] [stdout]    ✔  And p ← point(-0.4, 1, -0.1)
[INFO] [stdout]    ✔  When normal ← local_normal_at(c, p)
[INFO] [stdout]    ✔  Then normal = vector(0, 1, 0)
[INFO] [stdout]   Scenario Outline: The normal on the surface of a cube
[INFO] [stdout]    ✔  Given c ← cube()
[INFO] [stdout]    ✔  And p ← point(0.3, -1, -0.7)
[INFO] [stdout]    ✔  When normal ← local_normal_at(c, p)
[INFO] [stdout]    ✔  Then normal = vector(0, -1, 0)
[INFO] [stdout]   Scenario Outline: The normal on the surface of a cube
[INFO] [stdout]    ✔  Given c ← cube()
[INFO] [stdout]    ✔  And p ← point(-0.6, 0.3, 1)
[INFO] [stdout]    ✔  When normal ← local_normal_at(c, p)
[INFO] [stdout]    ✔  Then normal = vector(0, 0, 1)
[INFO] [stdout]   Scenario Outline: The normal on the surface of a cube
[INFO] [stdout]    ✔  Given c ← cube()
[INFO] [stdout]    ✔  And p ← point(0.4, 0.4, -1)
[INFO] [stdout]    ✔  When normal ← local_normal_at(c, p)
[INFO] [stdout]    ✔  Then normal = vector(0, 0, -1)
[INFO] [stdout]   Scenario Outline: The normal on the surface of a cube
[INFO] [stdout]    ✔  Given c ← cube()
[INFO] [stdout]    ✔  And p ← point(1, 1, 1)
[INFO] [stdout]    ✔  When normal ← local_normal_at(c, p)
[INFO] [stdout]    ✔  Then normal = vector(1, 0, 0)
[INFO] [stdout]   Scenario Outline: The normal on the surface of a cube
[INFO] [stdout]    ✔  Given c ← cube()
[INFO] [stdout]    ✔  And p ← point(-1, -1, -1)
[INFO] [stdout]    ✔  When normal ← local_normal_at(c, p)
[INFO] [stdout]    ✔  Then normal = vector(-1, 0, 0)
[INFO] [stdout] Feature: Cylinders
[INFO] [stdout]   Scenario Outline: A ray misses a cylinder
[INFO] [stdout]    ✔  Given cyl ← cylinder()
[INFO] [stdout]    ✔  And direction ← normalize(vector(0, 1, 0))
[INFO] [stdout]    ✔  And r ← ray(point(1, 0, 0), direction)
[INFO] [stdout]    ✔  When xs ← local_intersect(cyl, r)
[INFO] [stdout]    ✔  Then xs.count = 0
[INFO] [stdout]   Scenario Outline: A ray misses a cylinder
[INFO] [stdout]    ✔  Given cyl ← cylinder()
[INFO] [stdout]    ✔  And direction ← normalize(vector(0, 1, 0))
[INFO] [stdout]    ✔  And r ← ray(point(0, 0, 0), direction)
[INFO] [stdout]    ✔  When xs ← local_intersect(cyl, r)
[INFO] [stdout]    ✔  Then xs.count = 0
[INFO] [stdout]   Scenario Outline: A ray misses a cylinder
[INFO] [stdout]    ✔  Given cyl ← cylinder()
[INFO] [stdout]    ✔  And direction ← normalize(vector(1, 1, 1))
[INFO] [stdout]    ✔  And r ← ray(point(0, 0, -5), direction)
[INFO] [stdout]    ✔  When xs ← local_intersect(cyl, r)
[INFO] [stdout]    ✔  Then xs.count = 0
[INFO] [stdout]   Scenario Outline: A ray strikes a cylinder
[INFO] [stdout]    ✔  Given cyl ← cylinder()
[INFO] [stdout]    ✔  And direction ← normalize(vector(0, 0, 1))
[INFO] [stdout]    ✔  And r ← ray(point(1, 0, -5), direction)
[INFO] [stdout]    ✔  When xs ← local_intersect(cyl, r)
[INFO] [stdout]    ✔  Then xs.count = 2
[INFO] [stdout]    ✔  And xs[0].t = 5
[INFO] [stdout]    ✔  And xs[1].t = 5
[INFO] [stdout]   Scenario Outline: A ray strikes a cylinder
[INFO] [stdout]    ✔  Given cyl ← cylinder()
[INFO] [stdout]    ✔  And direction ← normalize(vector(0, 0, 1))
[INFO] [stdout]    ✔  And r ← ray(point(0, 0, -5), direction)
[INFO] [stdout]    ✔  When xs ← local_intersect(cyl, r)
[INFO] [stdout]    ✔  Then xs.count = 2
[INFO] [stdout]    ✔  And xs[0].t = 4
[INFO] [stdout]    ✔  And xs[1].t = 6
[INFO] [stdout]   Scenario Outline: A ray strikes a cylinder
[INFO] [stdout]    ✔  Given cyl ← cylinder()
[INFO] [stdout]    ✔  And direction ← normalize(vector(0.1, 1, 1))
[INFO] [stdout]    ✔  And r ← ray(point(0.5, 0, -5), direction)
[INFO] [stdout]    ✔  When xs ← local_intersect(cyl, r)
[INFO] [stdout]    ✔  Then xs.count = 2
[INFO] [stdout]    ✔  And xs[0].t = 6.80798
[INFO] [stdout]    ✔  And xs[1].t = 7.08872
[INFO] [stdout]   Scenario Outline: Normal vector on a cylinder
[INFO] [stdout]    ✔  Given cyl ← cylinder()
[INFO] [stdout]    ✔  When n ← local_normal_at(cyl, point(1, 0, 0))
[INFO] [stdout]    ✔  Then n = vector(1, 0, 0)
[INFO] [stdout]   Scenario Outline: Normal vector on a cylinder
[INFO] [stdout]    ✔  Given cyl ← cylinder()
[INFO] [stdout]    ✔  When n ← local_normal_at(cyl, point(0, 5, -1))
[INFO] [stdout]    ✔  Then n = vector(0, 0, -1)
[INFO] [stdout]   Scenario Outline: Normal vector on a cylinder
[INFO] [stdout]    ✔  Given cyl ← cylinder()
[INFO] [stdout]    ✔  When n ← local_normal_at(cyl, point(0, -2, 1))
[INFO] [stdout]    ✔  Then n = vector(0, 0, 1)
[INFO] [stdout]   Scenario Outline: Normal vector on a cylinder
[INFO] [stdout]    ✔  Given cyl ← cylinder()
[INFO] [stdout]    ✔  When n ← local_normal_at(cyl, point(-1, 1, 0))
[INFO] [stdout]    ✔  Then n = vector(-1, 0, 0)
[INFO] [stdout]   Scenario: The default minimum and maximum for a cylinder
[INFO] [stdout]    ✔  Given cyl ← cylinder()
[INFO] [stdout]    ✔  Then cyl.minimum = -infinity
[INFO] [stdout]    ✔  And cyl.maximum = infinity
[INFO] [stdout]   Scenario Outline: Intersecting a constrained cylinder
[INFO] [stdout]    ✔  Given cyl ← cylinder()
[INFO] [stdout]    ✔  And cyl.minimum ← 1
[INFO] [stdout]    ✔  And cyl.maximum ← 2
[INFO] [stdout]    ✔  And direction ← normalize(vector(0.1, 1, 0))
[INFO] [stdout]    ✔  And r ← ray(point(0, 1.5, 0), direction)
[INFO] [stdout]    ✔  When xs ← local_intersect(cyl, r)
[INFO] [stdout]    ✔  Then xs.count = 0
[INFO] [stdout]   Scenario Outline: Intersecting a constrained cylinder
[INFO] [stdout]    ✔  Given cyl ← cylinder()
[INFO] [stdout]    ✔  And cyl.minimum ← 1
[INFO] [stdout]    ✔  And cyl.maximum ← 2
[INFO] [stdout]    ✔  And direction ← normalize(vector(0, 0, 1))
[INFO] [stdout]    ✔  And r ← ray(point(0, 3, -5), direction)
[INFO] [stdout]    ✔  When xs ← local_intersect(cyl, r)
[INFO] [stdout]    ✔  Then xs.count = 0
[INFO] [stdout]   Scenario Outline: Intersecting a constrained cylinder
[INFO] [stdout]    ✔  Given cyl ← cylinder()
[INFO] [stdout]    ✔  And cyl.minimum ← 1
[INFO] [stdout]    ✔  And cyl.maximum ← 2
[INFO] [stdout]    ✔  And direction ← normalize(vector(0, 0, 1))
[INFO] [stdout]    ✔  And r ← ray(point(0, 0, -5), direction)
[INFO] [stdout]    ✔  When xs ← local_intersect(cyl, r)
[INFO] [stdout]    ✔  Then xs.count = 0
[INFO] [stdout]   Scenario Outline: Intersecting a constrained cylinder
[INFO] [stdout]    ✔  Given cyl ← cylinder()
[INFO] [stdout]    ✔  And cyl.minimum ← 1
[INFO] [stdout]    ✔  And cyl.maximum ← 2
[INFO] [stdout]    ✔  And direction ← normalize(vector(0, 0, 1))
[INFO] [stdout]    ✔  And r ← ray(point(0, 2, -5), direction)
[INFO] [stdout]    ✔  When xs ← local_intersect(cyl, r)
[INFO] [stdout]    ✔  Then xs.count = 0
[INFO] [stdout]   Scenario Outline: Intersecting a constrained cylinder
[INFO] [stdout]    ✔  Given cyl ← cylinder()
[INFO] [stdout]    ✔  And cyl.minimum ← 1
[INFO] [stdout]    ✔  And cyl.maximum ← 2
[INFO] [stdout]    ✔  And direction ← normalize(vector(0, 0, 1))
[INFO] [stdout]    ✔  And r ← ray(point(0, 1, -5), direction)
[INFO] [stdout]    ✔  When xs ← local_intersect(cyl, r)
[INFO] [stdout]    ✔  Then xs.count = 0
[INFO] [stdout]   Scenario Outline: Intersecting a constrained cylinder
[INFO] [stdout]    ✔  Given cyl ← cylinder()
[INFO] [stdout]    ✔  And cyl.minimum ← 1
[INFO] [stdout]    ✔  And cyl.maximum ← 2
[INFO] [stdout]    ✔  And direction ← normalize(vector(0, 0, 1))
[INFO] [stdout]    ✔  And r ← ray(point(0, 1.5, -2), direction)
[INFO] [stdout]    ✔  When xs ← local_intersect(cyl, r)
[INFO] [stdout]    ✔  Then xs.count = 2
[INFO] [stdout]   Scenario: The default closed value for a cylinder
[INFO] [stdout]    ✔  Given cyl ← cylinder()
[INFO] [stdout]    ✔  Then cyl.closed = false
[INFO] [stdout]   Scenario Outline: Intersecting the caps of a closed cylinder
[INFO] [stdout]    ✔  Given cyl ← cylinder()
[INFO] [stdout]    ✔  And cyl.minimum ← 1
[INFO] [stdout]    ✔  And cyl.maximum ← 2
[INFO] [stdout]    ✔  And cyl.closed ← true
[INFO] [stdout]    ✔  And direction ← normalize(vector(0, -1, 0))
[INFO] [stdout]    ✔  And r ← ray(point(0, 3, 0), direction)
[INFO] [stdout]    ✔  When xs ← local_intersect(cyl, r)
[INFO] [stdout]    ✔  Then xs.count = 2
[INFO] [stdout]   Scenario Outline: Intersecting the caps of a closed cylinder
[INFO] [stdout]    ✔  Given cyl ← cylinder()
[INFO] [stdout]    ✔  And cyl.minimum ← 1
[INFO] [stdout]    ✔  And cyl.maximum ← 2
[INFO] [stdout]    ✔  And cyl.closed ← true
[INFO] [stdout]    ✔  And direction ← normalize(vector(0, -1, 2))
[INFO] [stdout]    ✔  And r ← ray(point(0, 3, -2), direction)
[INFO] [stdout]    ✔  When xs ← local_intersect(cyl, r)
[INFO] [stdout]    ✔  Then xs.count = 2
[INFO] [stdout]   Scenario Outline: Intersecting the caps of a closed cylinder
[INFO] [stdout]    ✔  Given cyl ← cylinder()
[INFO] [stdout]    ✔  And cyl.minimum ← 1
[INFO] [stdout]    ✔  And cyl.maximum ← 2
[INFO] [stdout]    ✔  And cyl.closed ← true
[INFO] [stdout]    ✔  And direction ← normalize(vector(0, -1, 1))
[INFO] [stdout]    ✔  And r ← ray(point(0, 4, -2), direction)
[INFO] [stdout]    ✔  When xs ← local_intersect(cyl, r)
[INFO] [stdout]    ✔  Then xs.count = 2
[INFO] [stdout]   Scenario Outline: Intersecting the caps of a closed cylinder
[INFO] [stdout]    ✔  Given cyl ← cylinder()
[INFO] [stdout]    ✔  And cyl.minimum ← 1
[INFO] [stdout]    ✔  And cyl.maximum ← 2
[INFO] [stdout]    ✔  And cyl.closed ← true
[INFO] [stdout]    ✔  And direction ← normalize(vector(0, 1, 2))
[INFO] [stdout]    ✔  And r ← ray(point(0, 0, -2), direction)
[INFO] [stdout]    ✔  When xs ← local_intersect(cyl, r)
[INFO] [stdout]    ✔  Then xs.count = 2
[INFO] [stdout]   Scenario Outline: Intersecting the caps of a closed cylinder
[INFO] [stdout]    ✔  Given cyl ← cylinder()
[INFO] [stdout]    ✔  And cyl.minimum ← 1
[INFO] [stdout]    ✔  And cyl.maximum ← 2
[INFO] [stdout]    ✔  And cyl.closed ← true
[INFO] [stdout]    ✔  And direction ← normalize(vector(0, 1, 1))
[INFO] [stdout]    ✔  And r ← ray(point(0, -1, -2), direction)
[INFO] [stdout]    ✔  When xs ← local_intersect(cyl, r)
[INFO] [stdout]    ✔  Then xs.count = 2
[INFO] [stdout]   Scenario Outline: The normal vector on a cylinder's end caps
[INFO] [stdout]    ✔  Given cyl ← cylinder()
[INFO] [stdout]    ✔  And cyl.minimum ← 1
[INFO] [stdout]    ✔  And cyl.maximum ← 2
[INFO] [stdout]    ✔  And cyl.closed ← true
[INFO] [stdout]    ✔  When n ← local_normal_at(cyl, point(0, 1, 0))
[INFO] [stdout]    ✔  Then n = vector(0, -1, 0)
[INFO] [stdout]   Scenario Outline: The normal vector on a cylinder's end caps
[INFO] [stdout]    ✔  Given cyl ← cylinder()
[INFO] [stdout]    ✔  And cyl.minimum ← 1
[INFO] [stdout]    ✔  And cyl.maximum ← 2
[INFO] [stdout]    ✔  And cyl.closed ← true
[INFO] [stdout]    ✔  When n ← local_normal_at(cyl, point(0.5, 1, 0))
[INFO] [stdout]    ✔  Then n = vector(0, -1, 0)
[INFO] [stdout]   Scenario Outline: The normal vector on a cylinder's end caps
[INFO] [stdout]    ✔  Given cyl ← cylinder()
[INFO] [stdout]    ✔  And cyl.minimum ← 1
[INFO] [stdout]    ✔  And cyl.maximum ← 2
[INFO] [stdout]    ✔  And cyl.closed ← true
[INFO] [stdout]    ✔  When n ← local_normal_at(cyl, point(0, 1, 0.5))
[INFO] [stdout]    ✔  Then n = vector(0, -1, 0)
[INFO] [stdout]   Scenario Outline: The normal vector on a cylinder's end caps
[INFO] [stdout]    ✔  Given cyl ← cylinder()
[INFO] [stdout]    ✔  And cyl.minimum ← 1
[INFO] [stdout]    ✔  And cyl.maximum ← 2
[INFO] [stdout]    ✔  And cyl.closed ← true
[INFO] [stdout]    ✔  When n ← local_normal_at(cyl, point(0, 2, 0))
[INFO] [stdout]    ✔  Then n = vector(0, 1, 0)
[INFO] [stdout]   Scenario Outline: The normal vector on a cylinder's end caps
[INFO] [stdout]    ✔  Given cyl ← cylinder()
[INFO] [stdout]    ✔  And cyl.minimum ← 1
[INFO] [stdout]    ✔  And cyl.maximum ← 2
[INFO] [stdout]    ✔  And cyl.closed ← true
[INFO] [stdout]    ✔  When n ← local_normal_at(cyl, point(0.5, 2, 0))
[INFO] [stdout]    ✔  Then n = vector(0, 1, 0)
[INFO] [stdout]   Scenario Outline: The normal vector on a cylinder's end caps
[INFO] [stdout]    ✔  Given cyl ← cylinder()
[INFO] [stdout]    ✔  And cyl.minimum ← 1
[INFO] [stdout]    ✔  And cyl.maximum ← 2
[INFO] [stdout]    ✔  And cyl.closed ← true
[INFO] [stdout]    ✔  When n ← local_normal_at(cyl, point(0, 2, 0.5))
[INFO] [stdout]    ✔  Then n = vector(0, 1, 0)
[INFO] [stdout] Feature: Groups
[INFO] [stdout]   Scenario: Creating a new group
[INFO] [stdout]    ✔  Given g ← group()
[INFO] [stdout]    ✔  Then g.transform = identity_matrix
[INFO] [stdout]    ✔  And g is empty
[INFO] [stdout]   Scenario: Adding a child to a group
[INFO] [stdout]    ✔  Given g ← group()
[INFO] [stdout]    ✔  And s ← test_shape()
[INFO] [stdout]    ✔  When add_child(g, s)
[INFO] [stdout]    ✔  Then g is not empty
[INFO] [stdout]    ✔  And g includes s
[INFO] [stdout]    ✔  And s.parent = g
[INFO] [stdout]   Scenario: Intersecting a ray with an empty group
[INFO] [stdout]    ✔  Given g ← group()
[INFO] [stdout]    ✔  And r ← ray(point(0, 0, 0), vector(0, 0, 1))
[INFO] [stdout]    ✔  When xs ← local_intersect(g, r)
[INFO] [stdout]    ✔  Then xs is empty
[INFO] [stdout]   Scenario: Intersecting a ray with a nonempty group
[INFO] [stdout]    ✔  Given g ← group()
[INFO] [stdout]    ✔  And s1 ← sphere()
[INFO] [stdout]    ✔  And s2 ← sphere()
[INFO] [stdout]    ✔  And set_transform(s2, translation(0, 0, -3))
[INFO] [stdout]    ✔  And s3 ← sphere()
[INFO] [stdout]    ✔  And set_transform(s3, translation(5, 0, 0))
[INFO] [stdout]    ✔  And add_child(g, s1)
[INFO] [stdout]    ✔  And add_child(g, s2)
[INFO] [stdout]    ✔  And add_child(g, s3)
[INFO] [stdout]    ✔  When r ← ray(point(0, 0, -5), vector(0, 0, 1))
[INFO] [stdout]    ✔  And xs ← local_intersect(g, r)
[INFO] [stdout]    ✔  Then xs.count = 4
[INFO] [stdout]    ✔  And xs[0].object = s2
[INFO] [stdout]    ✔  And xs[1].object = s2
[INFO] [stdout]    ✔  And xs[2].object = s1
[INFO] [stdout]    ✔  And xs[3].object = s1
[INFO] [stdout]   Scenario: Intersecting a transformed group
[INFO] [stdout]    ✔  Given g ← group()
[INFO] [stdout]    ✔  And set_transform(g, scaling(2, 2, 2))
[INFO] [stdout]    ✔  And s ← sphere()
[INFO] [stdout]    ✔  And set_transform(s, translation(5, 0, 0))
[INFO] [stdout]    ✔  And add_child(g, s)
[INFO] [stdout]    ✔  When r ← ray(point(10, 0, -10), vector(0, 0, 1))
[INFO] [stdout]    ✔  And xs ← intersect(g, r)
[INFO] [stdout]    ✔  Then xs.count = 2
[INFO] [stdout] Feature: Intersections
[INFO] [stdout]   Scenario: An intersection encapsulates t and object
[INFO] [stdout]    ✔  Given s ← sphere
[INFO] [stdout]    ✔  When i ← intersection(3.5, s)
[INFO] [stdout]    ✔  Then i.t = 3.5
[INFO] [stdout]    ✔  And i.object = s
[INFO] [stdout]   Scenario: Precomputing the state of an intersection
[INFO] [stdout]    ✔  Given origin ← point(0, 0, -5)
[INFO] [stdout]    ✔  And direction ← vector(0, 0, 1)
[INFO] [stdout]    ✔  And r ← ray(origin, direction)
[INFO] [stdout]    ✔  And shape ← sphere
[INFO] [stdout]    ✔  And i ← intersection(4, shape)
[INFO] [stdout]    ✔  When comps ← prepare_computations(i, r)
[INFO] [stdout]    ✔  Then comps.t = i.t
[INFO] [stdout]    ✔  And comps.object = i.object
[INFO] [stdout]    ✔  And comps.point = point(0, 0, -1)
[INFO] [stdout]    ✔  And comps.eyev = vector(0, 0, -1)
[INFO] [stdout]    ✔  And comps.normalv = vector(0, 0, -1)
[INFO] [stdout]   Scenario: Precomputing the reflection vector
[INFO] [stdout]    ✔  Given shape ← plane
[INFO] [stdout]    ✔  And origin ← point(0, 1, -1)
[INFO] [stdout]    ✔  And direction ← vector(0.0, -0.70710678118, 0.70710678118)
[INFO] [stdout]    ✔  And r ← ray(origin, direction)
[INFO] [stdout]    ✔  And i ← intersection(0.70710678118, shape)
[INFO] [stdout]    ✔  When comps ← prepare_computations(i, r)
[INFO] [stdout]    ✔  Then comps.reflectv = vector(0.0, 0.70710678118, 0.70710678118)
[INFO] [stdout]   Scenario: The hit, when an intersection occurs on the outside
[INFO] [stdout]    ✔  Given origin ← point(0, 0, -5)
[INFO] [stdout]    ✔  And direction ← vector(0, 0, 1)
[INFO] [stdout]    ✔  And r ← ray(origin, direction)
[INFO] [stdout]    ✔  And shape ← sphere
[INFO] [stdout]    ✔  And i ← intersection(4, shape)
[INFO] [stdout]    ✔  When comps ← prepare_computations(i, r)
[INFO] [stdout]    ✔  Then comps.inside = false
[INFO] [stdout]   Scenario: The hit, when an intersection occurs on the inside
[INFO] [stdout]    ✔  Given origin ← point(0, 0, 0)
[INFO] [stdout]    ✔  And direction ← vector(0, 0, 1)
[INFO] [stdout]    ✔  And r ← ray(origin, direction)
[INFO] [stdout]    ✔  And shape ← sphere
[INFO] [stdout]    ✔  And i ← intersection(1, shape)
[INFO] [stdout]    ✔  When comps ← prepare_computations(i, r)
[INFO] [stdout]    ✔  Then comps.point = point(0, 0, 1)
[INFO] [stdout]    ✔  And comps.eyev = vector(0, 0, -1)
[INFO] [stdout]    ✔  And comps.inside = true
[INFO] [stdout]    ✔  And comps.normalv = vector(0, 0, -1)
[INFO] [stdout]   Scenario: The hit should offset the point
[INFO] [stdout]    ✔  Given origin ← point(0, 0, -5)
[INFO] [stdout]    ✔  And direction ← vector(0, 0, 1)
[INFO] [stdout]    ✔  And r ← ray(origin, direction)
[INFO] [stdout]    ✔  And shape ← sphere with:
[INFO] [stdout]        | transform | translation(0, 0, 1) |
[INFO] [stdout]    ✔  And i ← intersection(5, shape)
[INFO] [stdout]    ✔  When comps ← prepare_computations(i, r)
[INFO] [stdout]    ✔  Then comps.over_point.z < -EPSILON/2
[INFO] [stdout]    ✔  And comps.point.z > comps.over_point.z
[INFO] [stdout]   Scenario: The under point is offset below the surface
[INFO] [stdout]    ✔  Given origin ← point(0, 0, -5)
[INFO] [stdout]    ✔  And direction ← vector(0, 0, 1)
[INFO] [stdout]    ✔  And r ← ray(origin, direction)
[INFO] [stdout]    ✔  And shape ← glass_sphere() with:
[INFO] [stdout]        | transform | translation(0, 0, 1) |
[INFO] [stdout]    ✔  And i ← intersection(5, shape)
[INFO] [stdout]    ✔  And xs ← intersections(i)
[INFO] [stdout]    ✔  When comps ← prepare_computations(i, r, xs)
[INFO] [stdout]    ✔  Then comps.under_point.z > EPSILON/2
[INFO] [stdout]    ✔  And comps.point.z < comps.under_point.z
[INFO] [stdout]   Scenario: Aggregating intersections
[INFO] [stdout]    ✔  Given s ← sphere
[INFO] [stdout]    ✔  And i1 ← intersection(1, s)
[INFO] [stdout]    ✔  And i2 ← intersection(2, s)
[INFO] [stdout]    ✔  When xs ← intersections(i1, i2)
[INFO] [stdout]    ✔  Then xs.count = 2
[INFO] [stdout]    ✔  And xs[0].t = 1
[INFO] [stdout]    ✔  And xs[1].t = 2
[INFO] [stdout]   Scenario: The hit, when all intersections have positive t
[INFO] [stdout]    ✔  Given s ← sphere
[INFO] [stdout]    ✔  And i1 ← intersection(1, s)
[INFO] [stdout]    ✔  And i2 ← intersection(2, s)
[INFO] [stdout]    ✔  And xs ← intersections(i2, i1)
[INFO] [stdout]    ✔  When i ← hit(xs)
[INFO] [stdout]    ✔  Then i = i1
[INFO] [stdout]   Scenario: The hit, when some intersections have negative t
[INFO] [stdout]    ✔  Given s ← sphere
[INFO] [stdout]    ✔  And i1 ← intersection(-1, s)
[INFO] [stdout]    ✔  And i2 ← intersection(1, s)
[INFO] [stdout]    ✔  And xs ← intersections(i2, i1)
[INFO] [stdout]    ✔  When i ← hit(xs)
[INFO] [stdout]    ✔  Then i = i2
[INFO] [stdout]   Scenario: The hit, when all intersections have negative t
[INFO] [stdout]    ✔  Given s ← sphere
[INFO] [stdout]    ✔  And i1 ← intersection(-2, s)
[INFO] [stdout]    ✔  And i2 ← intersection(-1, s)
[INFO] [stdout]    ✔  And xs ← intersections(i2, i1)
[INFO] [stdout]    ✔  When i ← hit(xs)
[INFO] [stdout]    ✔  Then i is nothing
[INFO] [stdout]   Scenario: The hit is always the lowest nonnegative intersection
[INFO] [stdout]    ✔  Given s ← sphere
[INFO] [stdout]    ✔  And i1 ← intersection(5, s)
[INFO] [stdout]    ✔  And i2 ← intersection(7, s)
[INFO] [stdout]    ✔  And i3 ← intersection(-3, s)
[INFO] [stdout]    ✔  And i4 ← intersection(2, s)
[INFO] [stdout]    ✔  And xs ← intersections(i1, i2, i3, i4)
[INFO] [stdout]    ✔  When i ← hit(xs)
[INFO] [stdout]    ✔  Then i = i4
[INFO] [stdout]   Scenario Outline: Finding n1 and n2 at various intersections
[INFO] [stdout]    ✔  Given A ← glass_sphere() with:
[INFO] [stdout]        | transform                 | scaling(2, 2, 2) |
[INFO] [stdout]        | material.refractive_index | 1.5              |
[INFO] [stdout]    ✔  And B ← glass_sphere() with:
[INFO] [stdout]        | transform                 | translation(0, 0, -0.25) |
[INFO] [stdout]        | material.refractive_index | 2.0                      |
[INFO] [stdout]    ✔  And C ← glass_sphere() with:
[INFO] [stdout]        | transform                 | translation(0, 0, 0.25) |
[INFO] [stdout]        | material.refractive_index | 2.5                     |
[INFO] [stdout]    ✔  And r ← ray(point(0, 0, -4), vector(0, 0, 1))
[INFO] [stdout]    ✔  And xs ← intersections(2:A, 2.75:B, 3.25:C, 4.75:B, 5.25:C, 6:A)
[INFO] [stdout]    ✔  When comps ← prepare_computations(xs[0], r, xs)
[INFO] [stdout]    ✔  Then comps.n1 = 1.0
[INFO] [stdout]    ✔  And comps.n2 = 1.5
[INFO] [stdout]   Scenario Outline: Finding n1 and n2 at various intersections
[INFO] [stdout]    ✔  Given A ← glass_sphere() with:
[INFO] [stdout]        | transform                 | scaling(2, 2, 2) |
[INFO] [stdout]        | material.refractive_index | 1.5              |
[INFO] [stdout]    ✔  And B ← glass_sphere() with:
[INFO] [stdout]        | transform                 | translation(0, 0, -0.25) |
[INFO] [stdout]        | material.refractive_index | 2.0                      |
[INFO] [stdout]    ✔  And C ← glass_sphere() with:
[INFO] [stdout]        | transform                 | translation(0, 0, 0.25) |
[INFO] [stdout]        | material.refractive_index | 2.5                     |
[INFO] [stdout]    ✔  And r ← ray(point(0, 0, -4), vector(0, 0, 1))
[INFO] [stdout]    ✔  And xs ← intersections(2:A, 2.75:B, 3.25:C, 4.75:B, 5.25:C, 6:A)
[INFO] [stdout]    ✔  When comps ← prepare_computations(xs[1], r, xs)
[INFO] [stdout]    ✔  Then comps.n1 = 1.5
[INFO] [stdout]    ✔  And comps.n2 = 2.0
[INFO] [stdout]   Scenario Outline: Finding n1 and n2 at various intersections
[INFO] [stdout]    ✔  Given A ← glass_sphere() with:
[INFO] [stdout]        | transform                 | scaling(2, 2, 2) |
[INFO] [stdout]        | material.refractive_index | 1.5              |
[INFO] [stdout]    ✔  And B ← glass_sphere() with:
[INFO] [stdout]        | transform                 | translation(0, 0, -0.25) |
[INFO] [stdout]        | material.refractive_index | 2.0                      |
[INFO] [stdout]    ✔  And C ← glass_sphere() with:
[INFO] [stdout]        | transform                 | translation(0, 0, 0.25) |
[INFO] [stdout]        | material.refractive_index | 2.5                     |
[INFO] [stdout]    ✔  And r ← ray(point(0, 0, -4), vector(0, 0, 1))
[INFO] [stdout]    ✔  And xs ← intersections(2:A, 2.75:B, 3.25:C, 4.75:B, 5.25:C, 6:A)
[INFO] [stdout]    ✔  When comps ← prepare_computations(xs[2], r, xs)
[INFO] [stdout]    ✔  Then comps.n1 = 2.0
[INFO] [stdout]    ✔  And comps.n2 = 2.5
[INFO] [stdout]   Scenario Outline: Finding n1 and n2 at various intersections
[INFO] [stdout]    ✔  Given A ← glass_sphere() with:
[INFO] [stdout]        | transform                 | scaling(2, 2, 2) |
[INFO] [stdout]        | material.refractive_index | 1.5              |
[INFO] [stdout]    ✔  And B ← glass_sphere() with:
[INFO] [stdout]        | transform                 | translation(0, 0, -0.25) |
[INFO] [stdout]        | material.refractive_index | 2.0                      |
[INFO] [stdout]    ✔  And C ← glass_sphere() with:
[INFO] [stdout]        | transform                 | translation(0, 0, 0.25) |
[INFO] [stdout]        | material.refractive_index | 2.5                     |
[INFO] [stdout]    ✔  And r ← ray(point(0, 0, -4), vector(0, 0, 1))
[INFO] [stdout]    ✔  And xs ← intersections(2:A, 2.75:B, 3.25:C, 4.75:B, 5.25:C, 6:A)
[INFO] [stdout]    ✔  When comps ← prepare_computations(xs[3], r, xs)
[INFO] [stdout]    ✔  Then comps.n1 = 2.5
[INFO] [stdout]    ✔  And comps.n2 = 2.5
[INFO] [stdout]   Scenario Outline: Finding n1 and n2 at various intersections
[INFO] [stdout]    ✔  Given A ← glass_sphere() with:
[INFO] [stdout]        | transform                 | scaling(2, 2, 2) |
[INFO] [stdout]        | material.refractive_index | 1.5              |
[INFO] [stdout]    ✔  And B ← glass_sphere() with:
[INFO] [stdout]        | transform                 | translation(0, 0, -0.25) |
[INFO] [stdout]        | material.refractive_index | 2.0                      |
[INFO] [stdout]    ✔  And C ← glass_sphere() with:
[INFO] [stdout]        | transform                 | translation(0, 0, 0.25) |
[INFO] [stdout]        | material.refractive_index | 2.5                     |
[INFO] [stdout]    ✔  And r ← ray(point(0, 0, -4), vector(0, 0, 1))
[INFO] [stdout]    ✔  And xs ← intersections(2:A, 2.75:B, 3.25:C, 4.75:B, 5.25:C, 6:A)
[INFO] [stdout]    ✔  When comps ← prepare_computations(xs[4], r, xs)
[INFO] [stdout]    ✔  Then comps.n1 = 2.5
[INFO] [stdout]    ✔  And comps.n2 = 1.5
[INFO] [stdout]   Scenario Outline: Finding n1 and n2 at various intersections
[INFO] [stdout]    ✔  Given A ← glass_sphere() with:
[INFO] [stdout]        | transform                 | scaling(2, 2, 2) |
[INFO] [stdout]        | material.refractive_index | 1.5              |
[INFO] [stdout]    ✔  And B ← glass_sphere() with:
[INFO] [stdout]        | transform                 | translation(0, 0, -0.25) |
[INFO] [stdout]        | material.refractive_index | 2.0                      |
[INFO] [stdout]    ✔  And C ← glass_sphere() with:
[INFO] [stdout]        | transform                 | translation(0, 0, 0.25) |
[INFO] [stdout]        | material.refractive_index | 2.5                     |
[INFO] [stdout]    ✔  And r ← ray(point(0, 0, -4), vector(0, 0, 1))
[INFO] [stdout]    ✔  And xs ← intersections(2:A, 2.75:B, 3.25:C, 4.75:B, 5.25:C, 6:A)
[INFO] [stdout]    ✔  When comps ← prepare_computations(xs[5], r, xs)
[INFO] [stdout]    ✔  Then comps.n1 = 1.5
[INFO] [stdout]    ✔  And comps.n2 = 1.0
[INFO] [stdout]   Scenario: The Schlick approximation under total internal reflection
[INFO] [stdout]    ✔  Given shape ← glass_sphere()
[INFO] [stdout]    ✔  And r ← ray(point(0, 0, 0.70710678118), vector(0, 1, 0))
[INFO] [stdout]    ✔  And xs ← intersections(-0.70710678118:shape, 0.70710678118:shape)
[INFO] [stdout]    ✔  When comps ← prepare_computations(xs[1], r, xs)
[INFO] [stdout]    ✔  And reflectance ← schlick(comps)
[INFO] [stdout]    ✔  Then reflectance = 1.0
[INFO] [stdout]   Scenario: The Schlick approximation with a perpendicular viewing angle
[INFO] [stdout]    ✔  Given shape ← glass_sphere()
[INFO] [stdout]    ✔  And r ← ray(point(0, 0, 0), vector(0, 1, 0))
[INFO] [stdout]    ✔  And xs ← intersections(-1:shape, 1:shape)
[INFO] [stdout]    ✔  When comps ← prepare_computations(xs[1], r, xs)
[INFO] [stdout]    ✔  And reflectance ← schlick(comps)
[INFO] [stdout]    ✔  Then reflectance = 0.04
[INFO] [stdout]   Scenario: The Schlick approximation with small angle and n2 > n1
[INFO] [stdout]    ✔  Given shape ← glass_sphere()
[INFO] [stdout]    ✔  And r ← ray(point(0, 0.99, -2), vector(0, 0, 1))
[INFO] [stdout]    ✔  And xs ← intersections(1.8589:shape)
[INFO] [stdout]    ✔  When comps ← prepare_computations(xs[0], r, xs)
[INFO] [stdout]    ✔  And reflectance ← schlick(comps)
[INFO] [stdout]    ✔  Then reflectance = 0.48873
[INFO] [stdout]   Scenario: An intersection can encapsulate `u` and `v`
[INFO] [stdout]    ✔  Given s ← triangle(point(0, 1, 0), point(-1, 0, 0), point(1, 0, 0))
[INFO] [stdout]    ✔  When i ← intersection_with_uv(3.5, s, 0.2, 0.4)
[INFO] [stdout]    ✔  Then i.u = 0.2
[INFO] [stdout]    ✔  And i.v = 0.4
[INFO] [stdout] Feature: Lights
[INFO] [stdout]   Scenario: A point light has a position and intensity
[INFO] [stdout]    ✔  Given intensity ← color(1, 1, 1)
[INFO] [stdout]    ✔  And position ← point(0, 0, 0)
[INFO] [stdout]    ✔  When light ← point_light(position, intensity)
[INFO] [stdout]    ✔  Then light.position = position
[INFO] [stdout]    ✔  And light.intensity = intensity
[INFO] [stdout] Feature: Materials
[INFO] [stdout]   Scenario: The default material
[INFO] [stdout]    ✔> Given m ← material
[INFO] [stdout]    ✔> And position ← point(0, 0, 0)
[INFO] [stdout]    ✔> And intensity ← color(1, 1, 1)
[INFO] [stdout]    ✔> And black ← color(0, 0, 0)
[INFO] [stdout]    ✔> And white ← color(1, 1, 1)
[INFO] [stdout]    ✔  Given m ← material
[INFO] [stdout]    ✔  Then m.color = color(1, 1, 1)
[INFO] [stdout]    ✔  And m.ambient = 0.1
[INFO] [stdout]    ✔  And m.diffuse = 0.9
[INFO] [stdout]    ✔  And m.specular = 0.9
[INFO] [stdout]    ✔  And m.shininess = 200.0
[INFO] [stdout]   Scenario: Reflectivity for the default material
[INFO] [stdout]    ✔> Given m ← material
[INFO] [stdout]    ✔> And position ← point(0, 0, 0)
[INFO] [stdout]    ✔> And intensity ← color(1, 1, 1)
[INFO] [stdout]    ✔> And black ← color(0, 0, 0)
[INFO] [stdout]    ✔> And white ← color(1, 1, 1)
[INFO] [stdout]    ✔  Given m ← material
[INFO] [stdout]    ✔  Then m.reflective = 0.0
[INFO] [stdout]   Scenario: Transparency and Refractive Index for the default material
[INFO] [stdout]    ✔> Given m ← material
[INFO] [stdout]    ✔> And position ← point(0, 0, 0)
[INFO] [stdout]    ✔> And intensity ← color(1, 1, 1)
[INFO] [stdout]    ✔> And black ← color(0, 0, 0)
[INFO] [stdout]    ✔> And white ← color(1, 1, 1)
[INFO] [stdout]    ✔  Given m ← material
[INFO] [stdout]    ✔  Then m.transparency = 0.0
[INFO] [stdout]    ✔  And m.refractive_index = 1.0
[INFO] [stdout]   Scenario: Lighting with the eye between the light and the surface
[INFO] [stdout]    ✔> Given m ← material
[INFO] [stdout]    ✔> And position ← point(0, 0, 0)
[INFO] [stdout]    ✔> And intensity ← color(1, 1, 1)
[INFO] [stdout]    ✔> And black ← color(0, 0, 0)
[INFO] [stdout]    ✔> And white ← color(1, 1, 1)
[INFO] [stdout]    ✔  Given eyev ← vector(0, 0, -1)
[INFO] [stdout]    ✔  And normalv ← vector(0, 0, -1)
[INFO] [stdout]    ✔  And pos ← point(0, 0, -10)
[INFO] [stdout]    ✔  And light ← point_light(pos, intensity)
[INFO] [stdout]    ✔  When result ← lighting(m, light, position, eyev, normalv)
[INFO] [stdout]    ✔  Then result = color(1.9, 1.9, 1.9)
[INFO] [stdout]   Scenario: Lighting with the eye between light and surface, eye offset 45°
[INFO] [stdout]    ✔> Given m ← material
[INFO] [stdout]    ✔> And position ← point(0, 0, 0)
[INFO] [stdout]    ✔> And intensity ← color(1, 1, 1)
[INFO] [stdout]    ✔> And black ← color(0, 0, 0)
[INFO] [stdout]    ✔> And white ← color(1, 1, 1)
[INFO] [stdout]    ✔  Given eyev ← vector(0.0, 0.70710678118, -0.70710678118)
[INFO] [stdout]    ✔  And normalv ← vector(0, 0, -1)
[INFO] [stdout]    ✔  And pos ← point(0, 0, -10)
[INFO] [stdout]    ✔  And light ← point_light(pos, intensity)
[INFO] [stdout]    ✔  When result ← lighting(m, light, position, eyev, normalv)
[INFO] [stdout]    ✔  Then result = color(1.0, 1.0, 1.0)
[INFO] [stdout]   Scenario: Lighting with eye opposite surface, light offset 45°
[INFO] [stdout]    ✔> Given m ← material
[INFO] [stdout]    ✔> And position ← point(0, 0, 0)
[INFO] [stdout]    ✔> And intensity ← color(1, 1, 1)
[INFO] [stdout]    ✔> And black ← color(0, 0, 0)
[INFO] [stdout]    ✔> And white ← color(1, 1, 1)
[INFO] [stdout]    ✔  Given eyev ← vector(0, 0, -1)
[INFO] [stdout]    ✔  And normalv ← vector(0, 0, -1)
[INFO] [stdout]    ✔  And pos ← point(0, 10, -10)
[INFO] [stdout]    ✔  And light ← point_light(pos, intensity)
[INFO] [stdout]    ✔  When result ← lighting(m, light, position, eyev, normalv)
[INFO] [stdout]    ✔  Then result = color(0.7364, 0.7364, 0.7364)
[INFO] [stdout]   Scenario: Lighting with eye in the path of the reflection vector
[INFO] [stdout]    ✔> Given m ← material
[INFO] [stdout]    ✔> And position ← point(0, 0, 0)
[INFO] [stdout]    ✔> And intensity ← color(1, 1, 1)
[INFO] [stdout]    ✔> And black ← color(0, 0, 0)
[INFO] [stdout]    ✔> And white ← color(1, 1, 1)
[INFO] [stdout]    ✔  Given eyev ← vector(0.0, -0.70710678118, -0.70710678118)
[INFO] [stdout]    ✔  And normalv ← vector(0, 0, -1)
[INFO] [stdout]    ✔  And pos ← point(0, 10, -10)
[INFO] [stdout]    ✔  And light ← point_light(pos, intensity)
[INFO] [stdout]    ✔  When result ← lighting(m, light, position, eyev, normalv)
[INFO] [stdout]    ✔  Then result = color(1.6364, 1.6364, 1.6364)
[INFO] [stdout]   Scenario: Lighting with the light behind the surface
[INFO] [stdout]    ✔> Given m ← material
[INFO] [stdout]    ✔> And position ← point(0, 0, 0)
[INFO] [stdout]    ✔> And intensity ← color(1, 1, 1)
[INFO] [stdout]    ✔> And black ← color(0, 0, 0)
[INFO] [stdout]    ✔> And white ← color(1, 1, 1)
[INFO] [stdout]    ✔  Given eyev ← vector(0, 0, -1)
[INFO] [stdout]    ✔  And normalv ← vector(0, 0, -1)
[INFO] [stdout]    ✔  And pos ← point(0, 0, 10)
[INFO] [stdout]    ✔  And light ← point_light(pos, intensity)
[INFO] [stdout]    ✔  When result ← lighting(m, light, position, eyev, normalv)
[INFO] [stdout]    ✔  Then result = color(0.1, 0.1, 0.1)
[INFO] [stdout]   Scenario: Lighting with the surface in shadow
[INFO] [stdout]    ✔> Given m ← material
[INFO] [stdout]    ✔> And position ← point(0, 0, 0)
[INFO] [stdout]    ✔> And intensity ← color(1, 1, 1)
[INFO] [stdout]    ✔> And black ← color(0, 0, 0)
[INFO] [stdout]    ✔> And white ← color(1, 1, 1)
[INFO] [stdout]    ✔  Given eyev ← vector(0, 0, -1)
[INFO] [stdout]    ✔  And normalv ← vector(0, 0, -1)
[INFO] [stdout]    ✔  And pos ← point(0, 0, -10)
[INFO] [stdout]    ✔  And light ← point_light(pos, intensity)
[INFO] [stdout]    ✔  And in_shadow ← true
[INFO] [stdout]    ✔  When result ← lighting(m, light, position, eyev, normalv, in_shadow)
[INFO] [stdout]    ✔  Then result = color(0.1, 0.1, 0.1)
[INFO] [stdout]   Scenario: Lighting with a pattern applied
[INFO] [stdout]    ✔> Given m ← material
[INFO] [stdout]    ✔> And position ← point(0, 0, 0)
[INFO] [stdout]    ✔> And intensity ← color(1, 1, 1)
[INFO] [stdout]    ✔> And black ← color(0, 0, 0)
[INFO] [stdout]    ✔> And white ← color(1, 1, 1)
[INFO] [stdout]    ✔  Given m.pattern ← stripe_pattern(white, black)
[INFO] [stdout]    ✔  And m.ambient ← 1.0
[INFO] [stdout]    ✔  And m.diffuse ← 0.0
[INFO] [stdout]    ✔  And m.specular ← 0.0
[INFO] [stdout]    ✔  And eyev ← vector(0, 0, -1)
[INFO] [stdout]    ✔  And normalv ← vector(0, 0, -1)
[INFO] [stdout]    ✔  And position ← point(0, 0, -10)
[INFO] [stdout]    ✔  And light ← point_light(position, intensity)
[INFO] [stdout]    ✔  And p1 ← point(0.9, 0.0, 0.0)
[INFO] [stdout]    ✔  And p2 ← point(1.1, 0.0, 0.0)
[INFO] [stdout]    ✔  When c1 ← lighting(m, light, p1, eyev, normalv, false)
[INFO] [stdout]    ✔  And c2 ← lighting(m, light, p2, eyev, normalv, false)
[INFO] [stdout]    ✔  Then c1 = color(1, 1, 1)
[INFO] [stdout]    ✔  And c2 = color(0, 0, 0)
[INFO] [stdout] Feature: Matrices
[INFO] [stdout]   Scenario: Constructing and inspecting a 4x4 matrix
[INFO] [stdout]    ✔  Given the following 4x4 matrix M:
[INFO] [stdout]        | 1    | 2    | 3    | 4    |
[INFO] [stdout]        | 5.5  | 6.5  | 7.5  | 8.5  |
[INFO] [stdout]        | 9    | 10   | 11   | 12   |
[INFO] [stdout]        | 13.5 | 14.5 | 15.5 | 16.5 |
[INFO] [stdout]    ✔  Then M[0, 0] = 1
[INFO] [stdout]    ✔  And M[0, 3] = 4
[INFO] [stdout]    ✔  And M[1, 0] = 5.5
[INFO] [stdout]    ✔  And M[1, 2] = 7.5
[INFO] [stdout]    ✔  And M[2, 2] = 11
[INFO] [stdout]    ✔  And M[3, 0] = 13.5
[INFO] [stdout]    ✔  And M[3, 2] = 15.5
[INFO] [stdout]   Scenario: A 2x2 matrix ought to be representable
[INFO] [stdout]    ✔  Given the following 2x2 matrix M:
[INFO] [stdout]        | -3 | 5  |
[INFO] [stdout]        | 1  | -2 |
[INFO] [stdout]    ✔  Then M[0, 0] = -3
[INFO] [stdout]    ✔  And M[0, 1] = 5
[INFO] [stdout]    ✔  And M[1, 0] = 1
[INFO] [stdout]    ✔  And M[1, 1] = -2
[INFO] [stdout]   Scenario: A 3x3 matrix ought to be representable
[INFO] [stdout]    ✔  Given the following 3x3 matrix M:
[INFO] [stdout]        | -3 | 5  | 0  |
[INFO] [stdout]        | 1  | -2 | -7 |
[INFO] [stdout]        | 0  | 1  | 1  |
[INFO] [stdout]    ✔  Then M[0, 0] = -3
[INFO] [stdout]    ✔  And M[1, 1] = -2
[INFO] [stdout]    ✔  And M[2, 2] = 1
[INFO] [stdout]   Scenario: Matrix equality with identical matrices
[INFO] [stdout]    ✔  Given the following matrix A:
[INFO] [stdout]        | 1 | 2 | 3 | 4 |
[INFO] [stdout]        | 5 | 6 | 7 | 8 |
[INFO] [stdout]        | 9 | 8 | 7 | 6 |
[INFO] [stdout]        | 5 | 4 | 3 | 2 |
[INFO] [stdout]    ✔  And the following matrix B:
[INFO] [stdout]        | 1 | 2 | 3 | 4 |
[INFO] [stdout]        | 5 | 6 | 7 | 8 |
[INFO] [stdout]        | 9 | 8 | 7 | 6 |
[INFO] [stdout]        | 5 | 4 | 3 | 2 |
[INFO] [stdout]    ✔  Then A = B
[INFO] [stdout]   Scenario: Matrix equality with different matrices
[INFO] [stdout]    ✔  Given the following matrix A:
[INFO] [stdout]        | 1 | 2 | 3 | 4 |
[INFO] [stdout]        | 5 | 6 | 7 | 8 |
[INFO] [stdout]        | 9 | 8 | 7 | 6 |
[INFO] [stdout]        | 5 | 4 | 3 | 2 |
[INFO] [stdout]    ✔  And the following matrix B:
[INFO] [stdout]        | 2 | 3 | 4 | 5 |
[INFO] [stdout]        | 6 | 7 | 8 | 9 |
[INFO] [stdout]        | 8 | 7 | 6 | 5 |
[INFO] [stdout]        | 4 | 3 | 2 | 1 |
[INFO] [stdout]    ✔  Then A != B
[INFO] [stdout]   Scenario: Multiplying two matrices
[INFO] [stdout]    ✔  Given the following matrix A:
[INFO] [stdout]        | 1 | 2 | 3 | 4 |
[INFO] [stdout]        | 5 | 6 | 7 | 8 |
[INFO] [stdout]        | 9 | 8 | 7 | 6 |
[INFO] [stdout]        | 5 | 4 | 3 | 2 |
[INFO] [stdout]    ✔  And the following matrix B:
[INFO] [stdout]        | -2 | 1 | 2 | 3  |
[INFO] [stdout]        | 3  | 2 | 1 | -1 |
[INFO] [stdout]        | 4  | 3 | 6 | 5  |
[INFO] [stdout]        | 1  | 2 | 7 | 8  |
[INFO] [stdout]    ✔  Then A * B is the following 4x4 matrix:
[INFO] [stdout]        | 20 | 22 | 50  | 48  |
[INFO] [stdout]        | 44 | 54 | 114 | 108 |
[INFO] [stdout]        | 40 | 58 | 110 | 102 |
[INFO] [stdout]        | 16 | 26 | 46  | 42  |
[INFO] [stdout]   Scenario: A matrix multiplied by a tuple
[INFO] [stdout]    ✔  Given the following matrix A:
[INFO] [stdout]        | 1 | 2 | 3 | 4 |
[INFO] [stdout]        | 2 | 4 | 4 | 2 |
[INFO] [stdout]        | 8 | 6 | 4 | 1 |
[INFO] [stdout]        | 0 | 0 | 0 | 1 |
[INFO] [stdout]    ✔  And b ← tuple(1, 2, 3, 1)
[INFO] [stdout]    ✔  Then A * b = tuple(18, 24, 33, 1)
[INFO] [stdout]   Scenario: Multiplying a matrix by the identity matrix
[INFO] [stdout]    ✔  Given the following matrix A:
[INFO] [stdout]        | 0 | 1 | 2  | 4  |
[INFO] [stdout]        | 1 | 2 | 4  | 8  |
[INFO] [stdout]        | 2 | 4 | 8  | 16 |
[INFO] [stdout]        | 4 | 8 | 16 | 32 |
[INFO] [stdout]    ✔  Then A * identity_matrix = A
[INFO] [stdout]   Scenario: Multiplying the identity matrix by a tuple
[INFO] [stdout]    ✔  Given a ← tuple(1, 2, 3, 4)
[INFO] [stdout]    ✔  Then identity_matrix * a = a
[INFO] [stdout]   Scenario: Transposing a matrix
[INFO] [stdout]    ✔  Given the following matrix A:
[INFO] [stdout]        | 0 | 9 | 3 | 0 |
[INFO] [stdout]        | 9 | 8 | 0 | 8 |
[INFO] [stdout]        | 1 | 8 | 5 | 3 |
[INFO] [stdout]        | 0 | 0 | 5 | 8 |
[INFO] [stdout]    ✔  Then transpose(A) is the following matrix:
[INFO] [stdout]        | 0 | 9 | 1 | 0 |
[INFO] [stdout]        | 9 | 8 | 8 | 0 |
[INFO] [stdout]        | 3 | 0 | 5 | 5 |
[INFO] [stdout]        | 0 | 8 | 3 | 8 |
[INFO] [stdout]   Scenario: Transposing the identity matrix
[INFO] [stdout]    ✔  Given A ← transpose(identity_matrix)
[INFO] [stdout]    ✔  Then A = identity_matrix
[INFO] [stdout]   Scenario: Calculating the determinant of a 2x2 matrix
[INFO] [stdout]    ✔  Given the following 2x2 matrix A:
[INFO] [stdout]        | 1  | 5 |
[INFO] [stdout]        | -3 | 2 |
[INFO] [stdout]    ✔  Then determinant(A) = 17
[INFO] [stdout]   Scenario: A submatrix of a 3x3 matrix is a 2x2 matrix
[INFO] [stdout]    ✔  Given the following 3x3 matrix A:
[INFO] [stdout]        | 1  | 5 | 0  |
[INFO] [stdout]        | -3 | 2 | 7  |
[INFO] [stdout]        | 0  | 6 | -3 |
[INFO] [stdout]    ✔  Then submatrix(A, 0, 2) is the following 2x2 matrix:
[INFO] [stdout]        | -3 | 2 |
[INFO] [stdout]        | 0  | 6 |
[INFO] [stdout]   Scenario: A submatrix of a 4x4 matrix is a 3x3 matrix
[INFO] [stdout]    ✔  Given the following 4x4 matrix A:
[INFO] [stdout]        | -6 | 1 | 1  | 6 |
[INFO] [stdout]        | -8 | 5 | 8  | 6 |
[INFO] [stdout]        | -1 | 0 | 8  | 2 |
[INFO] [stdout]        | -7 | 1 | -1 | 1 |
[INFO] [stdout]    ✔  Then submatrix(A, 2, 1) is the following 3x3 matrix:
[INFO] [stdout]        | -6 | 1  | 6 |
[INFO] [stdout]        | -8 | 8  | 6 |
[INFO] [stdout]        | -7 | -1 | 1 |
[INFO] [stdout]   Scenario: Calculating a minor of a 3x3 matrix
[INFO] [stdout]    ✔  Given the following 3x3 matrix A:
[INFO] [stdout]        | 3 | 5  | 0  |
[INFO] [stdout]        | 2 | -1 | -7 |
[INFO] [stdout]        | 6 | -1 | 5  |
[INFO] [stdout]    ✔  And B ← submatrix(A, 1, 0)
[INFO] [stdout]    ✔  Then determinant(B) = 25
[INFO] [stdout]    ✔  And minor(A, 1, 0) = 25
[INFO] [stdout]   Scenario: Calculating a cofactor of a 3x3 matrix
[INFO] [stdout]    ✔  Given the following 3x3 matrix A:
[INFO] [stdout]        | 3 | 5  | 0  |
[INFO] [stdout]        | 2 | -1 | -7 |
[INFO] [stdout]        | 6 | -1 | 5  |
[INFO] [stdout]    ✔  Then minor(A, 0, 0) = -12
[INFO] [stdout]    ✔  And cofactor(A, 0, 0) = -12
[INFO] [stdout]    ✔  And minor(A, 1, 0) = 25
[INFO] [stdout]    ✔  And cofactor(A, 1, 0) = -25
[INFO] [stdout]   Scenario: Calculating the determinant of a 3x3 matrix
[INFO] [stdout]    ✔  Given the following 3x3 matrix A:
[INFO] [stdout]        | 1  | 2 | 6  |
[INFO] [stdout]        | -5 | 8 | -4 |
[INFO] [stdout]        | 2  | 6 | 4  |
[INFO] [stdout]    ✔  Then cofactor(A, 0, 0) = 56
[INFO] [stdout]    ✔  And cofactor(A, 0, 1) = 12
[INFO] [stdout]    ✔  And cofactor(A, 0, 2) = -46
[INFO] [stdout]    ✔  And determinant(A) = -196
[INFO] [stdout]   Scenario: Calculating the determinant of a 4x4 matrix
[INFO] [stdout]    ✔  Given the following 4x4 matrix A:
[INFO] [stdout]        | -2 | -8 | 3  | 5  |
[INFO] [stdout]        | -3 | 1  | 7  | 3  |
[INFO] [stdout]        | 1  | 2  | -9 | 6  |
[INFO] [stdout]        | -6 | 7  | 7  | -9 |
[INFO] [stdout]    ✔  Then cofactor(A, 0, 0) = 690
[INFO] [stdout]    ✔  And cofactor(A, 0, 1) = 447
[INFO] [stdout]    ✔  And cofactor(A, 0, 2) = 210
[INFO] [stdout]    ✔  And cofactor(A, 0, 3) = 51
[INFO] [stdout]    ✔  And determinant(A) = -4071
[INFO] [stdout]   Scenario: Testing an invertible matrix for invertibility
[INFO] [stdout]    ✔  Given the following 4x4 matrix A:
[INFO] [stdout]        | 6 | 4  | 4 | 4  |
[INFO] [stdout]        | 5 | 5  | 7 | 6  |
[INFO] [stdout]        | 4 | -9 | 3 | -7 |
[INFO] [stdout]        | 9 | 1  | 7 | -6 |
[INFO] [stdout]    ✔  Then determinant(A) = -2120
[INFO] [stdout]    ✔  And A is invertible
[INFO] [stdout]   Scenario: Testing a noninvertible matrix for invertibility
[INFO] [stdout]    ✔  Given the following 4x4 matrix A:
[INFO] [stdout]        | -4 | 2  | -2 | -3 |
[INFO] [stdout]        | 9  | 6  | 2  | 6  |
[INFO] [stdout]        | 0  | -5 | 1  | -5 |
[INFO] [stdout]        | 0  | 0  | 0  | 0  |
[INFO] [stdout]    ✔  Then determinant(A) = 0
[INFO] [stdout]    ✔  And A is not invertible
[INFO] [stdout]   Scenario: Calculating the inverse of a matrix
[INFO] [stdout]    ✔  Given the following 4x4 matrix A:
[INFO] [stdout]        | -5 | 2  | 6  | -8 |
[INFO] [stdout]        | 1  | -5 | 1  | 8  |
[INFO] [stdout]        | 7  | 7  | -6 | -7 |
[INFO] [stdout]        | 1  | -3 | 7  | 4  |
[INFO] [stdout]    ✔  And B ← inverse(A)
[INFO] [stdout]    ✔  Then determinant(A) = 532
[INFO] [stdout]    ✔  And cofactor(A, 2, 3) = -160
[INFO] [stdout]    ✔  And B[3, 2] = -160/532
[INFO] [stdout]    ✔  And cofactor(A, 3, 2) = 105
[INFO] [stdout]    ✔  And B[2, 3] = 105/532
[INFO] [stdout]    ✔  And B is the following 4x4 matrix:
[INFO] [stdout]        | 0.21805  | 0.45113  | 0.24060  | -0.04511 |
[INFO] [stdout]        | -0.80827 | -1.45677 | -0.44361 | 0.52068  |
[INFO] [stdout]        | -0.07895 | -0.22368 | -0.05263 | 0.19737  |
[INFO] [stdout]        | -0.52256 | -0.81391 | -0.30075 | 0.30639  |
[INFO] [stdout]   Scenario: Calculating the inverse of another matrix
[INFO] [stdout]    ✔  Given the following 4x4 matrix A:
[INFO] [stdout]        | 8  | -5 | 9  | 2  |
[INFO] [stdout]        | 7  | 5  | 6  | 1  |
[INFO] [stdout]        | -6 | 0  | 9  | 6  |
[INFO] [stdout]        | -3 | 0  | -9 | -4 |
[INFO] [stdout]    ✔  Then inverse(A) is the following 4x4 matrix:
[INFO] [stdout]        | -0.15385 | -0.15385 | -0.28205 | -0.53846 |
[INFO] [stdout]        | -0.07692 | 0.12308  | 0.02564  | 0.03077  |
[INFO] [stdout]        | 0.35897  | 0.35897  | 0.43590  | 0.92308  |
[INFO] [stdout]        | -0.69231 | -0.69231 | -0.76923 | -1.92308 |
[INFO] [stdout]   Scenario: Calculating the inverse of a third matrix
[INFO] [stdout]    ✔  Given the following 4x4 matrix A:
[INFO] [stdout]        | 9  | 3  | 0  | 9  |
[INFO] [stdout]        | -5 | -2 | -6 | -3 |
[INFO] [stdout]        | -4 | 9  | 6  | 4  |
[INFO] [stdout]        | -7 | 6  | 6  | 2  |
[INFO] [stdout]    ✔  Then inverse(A) is the following 4x4 matrix:
[INFO] [stdout]        | -0.04074 | -0.07778 | 0.14444  | -0.22222 |
[INFO] [stdout]        | -0.07778 | 0.03333  | 0.36667  | -0.33333 |
[INFO] [stdout]        | -0.02901 | -0.14630 | -0.10926 | 0.12963  |
[INFO] [stdout]        | 0.17778  | 0.06667  | -0.26667 | 0.33333  |
[INFO] [stdout]   Scenario: Multiplying a product by its inverse
[INFO] [stdout]    ✔  Given the following 4x4 matrix A:
[INFO] [stdout]        | 3  | -9 | 7  | 3  |
[INFO] [stdout]        | 3  | -8 | 2  | -9 |
[INFO] [stdout]        | -4 | 4  | 4  | 1  |
[INFO] [stdout]        | -6 | 5  | -1 | 1  |
[INFO] [stdout]    ✔  And the following 4x4 matrix B:
[INFO] [stdout]        | 8 | 2  | 2 | 2 |
[INFO] [stdout]        | 3 | -1 | 7 | 0 |
[INFO] [stdout]        | 7 | 0  | 5 | 4 |
[INFO] [stdout]        | 6 | -2 | 0 | 5 |
[INFO] [stdout]    ✔  And C ← A * B
[INFO] [stdout]    ✔  Then C * inverse(B) = A
[INFO] [stdout] Feature: OBJ File Parser
[INFO] [stdout]   Scenario: Ignoring unrecognized lines
[INFO] [stdout]    ✔  Given gibberish ← a file containing:
[INFO] [stdout]    ✔  When parser ← parse_obj_file(gibberish)
[INFO] [stdout]    ✔  Then parser should have ignored 5 lines
[INFO] [stdout]   Scenario: Vertex records
[INFO] [stdout]    ✔  Given file ← a file containing:
[INFO] [stdout]    ✔  When parser ← parse_obj_file(file)
[INFO] [stdout]    ✔  Then parser.vertices[1] = point(-1, 1, 0)
[INFO] [stdout]    ✔  And parser.vertices[2] = point(-1, 0.5, 0)
[INFO] [stdout]    ✔  And parser.vertices[3] = point(1, 0, 0)
[INFO] [stdout]    ✔  And parser.vertices[4] = point(1, 1, 0)
[INFO] [stdout]   Scenario: Parsing triangle faces
[INFO] [stdout]    ✔  Given file ← a file containing:
[INFO] [stdout]    ✔  When parser ← parse_obj_file(file)
[INFO] [stdout]    ✔  And g ← parser.default_group
[INFO] [stdout]    ✔  And t1 ← first child of g
[INFO] [stdout]    ✔  And t2 ← second child of g
[INFO] [stdout]    ✔  Then t1.p1 = parser.vertices[1]
[INFO] [stdout]    ✔  And t1.p2 = parser.vertices[2]
[INFO] [stdout]    ✔  And t1.p3 = parser.vertices[3]
[INFO] [stdout]    ✔  And t2.p1 = parser.vertices[1]
[INFO] [stdout]    ✔  And t2.p2 = parser.vertices[3]
[INFO] [stdout]    ✔  And t2.p3 = parser.vertices[4]
[INFO] [stdout]   Scenario: Triangulating polygons
[INFO] [stdout]    ✔  Given file ← a file containing:
[INFO] [stdout]    ✔  When parser ← parse_obj_file(file)
[INFO] [stdout]    ✔  And g ← parser.default_group
[INFO] [stdout]    ✔  And t1 ← first child of g
[INFO] [stdout]    ✔  And t2 ← second child of g
[INFO] [stdout]    ✔  And t3 ← third child of g
[INFO] [stdout]    ✔  Then t1.p1 = parser.vertices[1]
[INFO] [stdout]    ✔  And t1.p2 = parser.vertices[2]
[INFO] [stdout]    ✔  And t1.p3 = parser.vertices[3]
[INFO] [stdout]    ✔  And t2.p1 = parser.vertices[1]
[INFO] [stdout]    ✔  And t2.p2 = parser.vertices[3]
[INFO] [stdout]    ✔  And t2.p3 = parser.vertices[4]
[INFO] [stdout]    ✔  And t3.p1 = parser.vertices[1]
[INFO] [stdout]    ✔  And t3.p2 = parser.vertices[4]
[INFO] [stdout]    ✔  And t3.p3 = parser.vertices[5]
[INFO] [stdout]   Scenario: Triangles in groups
[INFO] [stdout]    ✔  Given file ← the file "triangles.obj"
[INFO] [stdout]    ✔  When parser ← parse_obj_file(file)
[INFO] [stdout]    ✔  And g1 ← "FirstGroup" from parser
[INFO] [stdout]    ✔  And g2 ← "SecondGroup" from parser
[INFO] [stdout]    ✔  And t1 ← first child of g1
[INFO] [stdout]    ✔  And t2 ← first child of g2
[INFO] [stdout]    ✔  Then t1.p1 = parser.vertices[1]
[INFO] [stdout]    ✔  And t1.p2 = parser.vertices[2]
[INFO] [stdout]    ✔  And t1.p3 = parser.vertices[3]
[INFO] [stdout]    ✔  And t2.p1 = parser.vertices[1]
[INFO] [stdout]    ✔  And t2.p2 = parser.vertices[3]
[INFO] [stdout]    ✔  And t2.p3 = parser.vertices[4]
[INFO] [stdout]   Scenario: Converting an OBJ file to a group
[INFO] [stdout]    ✔  Given file ← the file "triangles.obj"
[INFO] [stdout]    ✔  And parser ← parse_obj_file(file)
[INFO] [stdout]    ✔  When g ← obj_to_group(parser)
[INFO] [stdout]    ✔  Then g includes "FirstGroup" from parser
[INFO] [stdout]    ✔  And g includes "SecondGroup" from parser
[INFO] [stdout]   Scenario: Vertex normal records
[INFO] [stdout]    ✔  Given file ← a file containing:
[INFO] [stdout]    ✔  When parser ← parse_obj_file(file)
[INFO] [stdout]    ✔  Then parser.normals[1] = vector(0, 0, 1)
[INFO] [stdout]    ✔  And parser.normals[2] = vector(0.707, 0, -0.707)
[INFO] [stdout]    ✔  And parser.normals[3] = vector(1, 2, 3)
[INFO] [stdout]   Scenario: Faces with normals
[INFO] [stdout]    ✔  Given file ← a file containing:
[INFO] [stdout]    ✔  When parser ← parse_obj_file(file)
[INFO] [stdout]    ✔  And g ← parser.default_group
[INFO] [stdout]    ✔  And t1 ← first child of g
[INFO] [stdout]    ✔  And t2 ← second child of g
[INFO] [stdout]    ✔  Then t1.p1 = parser.vertices[1]
[INFO] [stdout]    ✔  And t1.p2 = parser.vertices[2]
[INFO] [stdout]    ✔  And t1.p3 = parser.vertices[3]
[INFO] [stdout]    ✔  And t1.n1 = parser.normals[3]
[INFO] [stdout]    ✔  And t1.n2 = parser.normals[1]
[INFO] [stdout]    ✔  And t1.n3 = parser.normals[2]
[INFO] [stdout]    ✔  And t2 = t1
[INFO] [stdout] Feature: Patterns
[INFO] [stdout]   Scenario: Creating a stripe pattern
[INFO] [stdout]    ✔> Given black ← color(0, 0, 0)
[INFO] [stdout]    ✔> And white ← color(1, 1, 1)
[INFO] [stdout]    ✔  Given pattern ← stripe_pattern(white, black)
[INFO] [stdout]    ✔  Then pattern.a = white
[INFO] [stdout]    ✔  And pattern.b = black
[INFO] [stdout]   Scenario: A stripe pattern is constant in y
[INFO] [stdout]    ✔> Given black ← color(0, 0, 0)
[INFO] [stdout]    ✔> And white ← color(1, 1, 1)
[INFO] [stdout]    ✔  Given pattern ← stripe_pattern(white, black)
[INFO] [stdout]    ✔  And p1 ← point(0, 0, 0)
[INFO] [stdout]    ✔  And p2 ← point(0, 1, 0)
[INFO] [stdout]    ✔  And p3 ← point(0, 2, 0)
[INFO] [stdout]    ✔  Then stripe_at(pattern, p1) = white
[INFO] [stdout]    ✔  And stripe_at(pattern, p2) = white
[INFO] [stdout]    ✔  And stripe_at(pattern, p3) = white
[INFO] [stdout]   Scenario: A stripe pattern is constant in z
[INFO] [stdout]    ✔> Given black ← color(0, 0, 0)
[INFO] [stdout]    ✔> And white ← color(1, 1, 1)
[INFO] [stdout]    ✔  Given pattern ← stripe_pattern(white, black)
[INFO] [stdout]    ✔  And p1 ← point(0, 0, 0)
[INFO] [stdout]    ✔  And p2 ← point(0, 0, 1)
[INFO] [stdout]    ✔  And p3 ← point(0, 0, 2)
[INFO] [stdout]    ✔  Then stripe_at(pattern, p1) = white
[INFO] [stdout]    ✔  And stripe_at(pattern, p2) = white
[INFO] [stdout]    ✔  And stripe_at(pattern, p3) = white
[INFO] [stdout]   Scenario: A stripe pattern alternates in x
[INFO] [stdout]    ✔> Given black ← color(0, 0, 0)
[INFO] [stdout]    ✔> And white ← color(1, 1, 1)
[INFO] [stdout]    ✔  Given pattern ← stripe_pattern(white, black)
[INFO] [stdout]    ✔  And p1 ← point(0, 0, 0)
[INFO] [stdout]    ✔  And p2 ← point(0.9, 0.0, 0.0)
[INFO] [stdout]    ✔  And p3 ← point(1, 0, 0)
[INFO] [stdout]    ✔  And p4 ← point(-0.1, 0.0, 0.0)
[INFO] [stdout]    ✔  And p5 ← point(-1, 0, 0)
[INFO] [stdout]    ✔  And p6 ← point(-1.1, 0.0, 0.0)
[INFO] [stdout]    ✔  Then stripe_at(pattern, p1) = white
[INFO] [stdout]    ✔  And stripe_at(pattern, p2) = white
[INFO] [stdout]    ✔  And stripe_at(pattern, p3) = black
[INFO] [stdout]    ✔  And stripe_at(pattern, p4) = black
[INFO] [stdout]    ✔  And stripe_at(pattern, p5) = black
[INFO] [stdout]    ✔  And stripe_at(pattern, p6) = white
[INFO] [stdout]   Scenario: Stripes with an object transformation
[INFO] [stdout]    ✔> Given black ← color(0, 0, 0)
[INFO] [stdout]    ✔> And white ← color(1, 1, 1)
[INFO] [stdout]    ✔  Given object ← sphere
[INFO] [stdout]    ✔  And transform ← scaling(2, 2, 2)
[INFO] [stdout]    ✔  And set_transform(object, transform)
[INFO] [stdout]    ✔  And pattern ← stripe_pattern(white, black)
[INFO] [stdout]    ✔  And p1 ← point(1.5, 0, 0)
[INFO] [stdout]    ✔  When c ← stripe_at_object(pattern, object, p1)
[INFO] [stdout]    ✔  Then c = white
[INFO] [stdout]   Scenario: Stripes with a pattern transformation
[INFO] [stdout]    ✔> Given black ← color(0, 0, 0)
[INFO] [stdout]    ✔> And white ← color(1, 1, 1)
[INFO] [stdout]    ✔  Given object ← sphere
[INFO] [stdout]    ✔  And pattern ← stripe_pattern(white, black)
[INFO] [stdout]    ✔  And transform ← scaling(2, 2, 2)
[INFO] [stdout]    ✔  And set_pattern_transform(pattern, transform)
[INFO] [stdout]    ✔  And p1 ← point(1.5, 0, 0)
[INFO] [stdout]    ✔  When c ← stripe_at_object(pattern, object, p1)
[INFO] [stdout]    ✔  Then c = white
[INFO] [stdout]   Scenario: Stripes with both an object and a pattern transformation
[INFO] [stdout]    ✔> Given black ← color(0, 0, 0)
[INFO] [stdout]    ✔> And white ← color(1, 1, 1)
[INFO] [stdout]    ✔  Given object ← sphere
[INFO] [stdout]    ✔  And transform ← scaling(2, 2, 2)
[INFO] [stdout]    ✔  And set_transform(object, transform)
[INFO] [stdout]    ✔  And pattern ← stripe_pattern(white, black)
[INFO] [stdout]    ✔  And t ← translation(0.5, 0.0, 0.0)
[INFO] [stdout]    ✔  And set_pattern_transform(pattern, t)
[INFO] [stdout]    ✔  And p1 ← point(2.5, 0, 0)
[INFO] [stdout]    ✔  When c ← stripe_at_object(pattern, object, p1)
[INFO] [stdout]    ✔  Then c = white
[INFO] [stdout]   Scenario: The default pattern transformation
[INFO] [stdout]    ✔> Given black ← color(0, 0, 0)
[INFO] [stdout]    ✔> And white ← color(1, 1, 1)
[INFO] [stdout]    ✔  Given pattern ← test_pattern
[INFO] [stdout]    ✔  Then pattern.transform = identity_matrix
[INFO] [stdout]   Scenario: Assigning a transformation
[INFO] [stdout]    ✔> Given black ← color(0, 0, 0)
[INFO] [stdout]    ✔> And white ← color(1, 1, 1)
[INFO] [stdout]    ✔  Given pattern ← test_pattern
[INFO] [stdout]    ✔  And transform ← translation(1, 2, 3)
[INFO] [stdout]    ✔  When set_pattern_transform(pattern, transform)
[INFO] [stdout]    ✔  Then pattern.transform = transform
[INFO] [stdout]   Scenario: A pattern with an object transformation
[INFO] [stdout]    ✔> Given black ← color(0, 0, 0)
[INFO] [stdout]    ✔> And white ← color(1, 1, 1)
[INFO] [stdout]    ✔  Given shape ← sphere
[INFO] [stdout]    ✔  And transform ← scaling(2, 2, 2)
[INFO] [stdout]    ✔  And set_transform(shape, transform)
[INFO] [stdout]    ✔  And pattern ← test_pattern
[INFO] [stdout]    ✔  And p1 ← point(2, 3, 4)
[INFO] [stdout]    ✔  When c ← pattern_at_shape(pattern, shape, p1)
[INFO] [stdout]    ✔  Then c = color(1.0, 1.5, 2.0)
[INFO] [stdout]   Scenario: A pattern with a pattern transformation
[INFO] [stdout]    ✔> Given black ← color(0, 0, 0)
[INFO] [stdout]    ✔> And white ← color(1, 1, 1)
[INFO] [stdout]    ✔  Given shape ← sphere
[INFO] [stdout]    ✔  And pattern ← test_pattern
[INFO] [stdout]    ✔  And t ← scaling(2, 2, 2)
[INFO] [stdout]    ✔  And set_pattern_transform(pattern, t)
[INFO] [stdout]    ✔  And p1 ← point(2, 3, 4)
[INFO] [stdout]    ✔  When c ← pattern_at_shape(pattern, shape, p1)
[INFO] [stdout]    ✔  Then c = color(1.0, 1.5, 2.0)
[INFO] [stdout]   Scenario: A pattern with both an object and a pattern transformation
[INFO] [stdout]    ✔> Given black ← color(0, 0, 0)
[INFO] [stdout]    ✔> And white ← color(1, 1, 1)
[INFO] [stdout]    ✔  Given shape ← sphere
[INFO] [stdout]    ✔  And transform ← scaling(2, 2, 2)
[INFO] [stdout]    ✔  And set_transform(shape, transform)
[INFO] [stdout]    ✔  And pattern ← test_pattern
[INFO] [stdout]    ✔  And t ← translation(0.5, 1, 1.5)
[INFO] [stdout]    ✔  And set_pattern_transform(pattern, t)
[INFO] [stdout]    ✔  And p1 ← point(2.5, 3.0, 3.5)
[INFO] [stdout]    ✔  When c ← pattern_at_shape(pattern, shape, p1)
[INFO] [stdout]    ✔  Then c = color(0.75, 0.5, 0.25)
[INFO] [stdout]   Scenario: A gradient linearly interpolates between colors
[INFO] [stdout]    ✔> Given black ← color(0, 0, 0)
[INFO] [stdout]    ✔> And white ← color(1, 1, 1)
[INFO] [stdout]    ✔  Given pattern ← gradient_pattern(white, black)
[INFO] [stdout]    ✔  And p1 ← point(0, 0, 0)
[INFO] [stdout]    ✔  And p2 ← point(0.25, 0.0, 0.0)
[INFO] [stdout]    ✔  And p3 ← point(0.5, 0.0, 0.0)
[INFO] [stdout]    ✔  And p4 ← point(0.75, 0.0, 0.0)
[INFO] [stdout]    ✔  Then pattern_at(pattern, p1) = white
[INFO] [stdout]    ✔  And pattern_at(pattern, p2) = color(0.75, 0.75, 0.75)
[INFO] [stdout]    ✔  And pattern_at(pattern, p3) = color(0.5, 0.5, 0.5)
[INFO] [stdout]    ✔  And pattern_at(pattern, p4) = color(0.25, 0.25, 0.25)
[INFO] [stdout]   Scenario: A ring should extend in both x and z
[INFO] [stdout]    ✔> Given black ← color(0, 0, 0)
[INFO] [stdout]    ✔> And white ← color(1, 1, 1)
[INFO] [stdout]    ✔  Given pattern ← ring_pattern(white, black)
[INFO] [stdout]    ✔  And p1 ← point(0, 0, 0)
[INFO] [stdout]    ✔  And p2 ← point(1, 0, 0)
[INFO] [stdout]    ✔  And p3 ← point(0, 0, 1)
[INFO] [stdout]    ✔  And p4 ← point(0.708, 0, 0.708)
[INFO] [stdout]    ✔  Then pattern_at(pattern, p1) = white
[INFO] [stdout]    ✔  And pattern_at(pattern, p2) = black
[INFO] [stdout]    ✔  And pattern_at(pattern, p3) = black
[INFO] [stdout]    ✔  And pattern_at(pattern, p4) = black
[INFO] [stdout]   Scenario: Checkers should repeat in x
[INFO] [stdout]    ✔> Given black ← color(0, 0, 0)
[INFO] [stdout]    ✔> And white ← color(1, 1, 1)
[INFO] [stdout]    ✔  Given pattern ← checkers_pattern(white, black)
[INFO] [stdout]    ✔  And p1 ← point(0, 0, 0)
[INFO] [stdout]    ✔  And p2 ← point(0.99, 0, 0)
[INFO] [stdout]    ✔  And p3 ← point(1.01, 0, 0)
[INFO] [stdout]    ✔  Then pattern_at(pattern, p1) = white
[INFO] [stdout]    ✔  And pattern_at(pattern, p2) = white
[INFO] [stdout]    ✔  And pattern_at(pattern, p3) = black
[INFO] [stdout]   Scenario: Checkers should repeat in y
[INFO] [stdout]    ✔> Given black ← color(0, 0, 0)
[INFO] [stdout]    ✔> And white ← color(1, 1, 1)
[INFO] [stdout]    ✔  Given pattern ← checkers_pattern(white, black)
[INFO] [stdout]    ✔  And p1 ← point(0, 0, 0)
[INFO] [stdout]    ✔  And p2 ← point(0, 0.99, 0)
[INFO] [stdout]    ✔  And p3 ← point(0, 1.01, 0)
[INFO] [stdout]    ✔  Then pattern_at(pattern, p1) = white
[INFO] [stdout]    ✔  And pattern_at(pattern, p2) = white
[INFO] [stdout]    ✔  And pattern_at(pattern, p3) = black
[INFO] [stdout]   Scenario: Checkers should repeat in z
[INFO] [stdout]    ✔> Given black ← color(0, 0, 0)
[INFO] [stdout]    ✔> And white ← color(1, 1, 1)
[INFO] [stdout]    ✔  Given pattern ← checkers_pattern(white, black)
[INFO] [stdout]    ✔  And p1 ← point(0, 0, 0)
[INFO] [stdout]    ✔  And p2 ← point(0, 0, 0.99)
[INFO] [stdout]    ✔  And p3 ← point(0, 0, 1.01)
[INFO] [stdout]    ✔  Then pattern_at(pattern, p1) = white
[INFO] [stdout]    ✔  And pattern_at(pattern, p2) = white
[INFO] [stdout]    ✔  And pattern_at(pattern, p3) = black
[INFO] [stdout] Feature: Planes
[INFO] [stdout]   Scenario: The normal of a plane is constant everywhere
[INFO] [stdout]    ✔  Given pl ← plane
[INFO] [stdout]    ✔  When p ← point(0, 0, 0)
[INFO] [stdout]    ✔  And n1 ← local_normal_at(pl, p)
[INFO] [stdout]    ✔  And p ← point(10, 0, -10)
[INFO] [stdout]    ✔  And n2 ← local_normal_at(pl, p)
[INFO] [stdout]    ✔  And p ← point(-5, 0, 150)
[INFO] [stdout]    ✔  And n3 ← local_normal_at(pl, p)
[INFO] [stdout]    ✔  Then n1 = vector(0, 1, 0)
[INFO] [stdout]    ✔  And n2 = vector(0, 1, 0)
[INFO] [stdout]    ✔  And n3 = vector(0, 1, 0)
[INFO] [stdout]   Scenario: Intersect with a ray parallel to the plane
[INFO] [stdout]    ✔  Given pl ← plane
[INFO] [stdout]    ✔  And origin ← point(0, 10, 0)
[INFO] [stdout]    ✔  And direction ← vector(0, 0, 1)
[INFO] [stdout]    ✔  And r ← ray(origin, direction)
[INFO] [stdout]    ✔  When xs ← local_intersect(p, r)
[INFO] [stdout]    ✔  Then xs is empty
[INFO] [stdout]   Scenario: Intersect with a coplanar ray
[INFO] [stdout]    ✔  Given pl ← plane
[INFO] [stdout]    ✔  And origin ← point(0, 0, 0)
[INFO] [stdout]    ✔  And direction ← vector(0, 0, 1)
[INFO] [stdout]    ✔  And r ← ray(origin, direction)
[INFO] [stdout]    ✔  When xs ← local_intersect(p, r)
[INFO] [stdout]    ✔  Then xs is empty
[INFO] [stdout]   Scenario: A ray intersecting a plane from above
[INFO] [stdout]    ✔  Given pl ← plane
[INFO] [stdout]    ✔  And origin ← point(0, 1, 0)
[INFO] [stdout]    ✔  And direction ← vector(0, -1, 0)
[INFO] [stdout]    ✔  And r ← ray(origin, direction)
[INFO] [stdout]    ✔  When xs ← local_intersect(p, r)
[INFO] [stdout]    ✔  Then xs.count = 1
[INFO] [stdout]    ✔  And xs[0].t = 1
[INFO] [stdout]    ✔  And xs[0].object = pl
[INFO] [stdout]   Scenario: A ray intersecting a plane from below
[INFO] [stdout]    ✔  Given pl ← plane
[INFO] [stdout]    ✔  And origin ← point(0, -1, 0)
[INFO] [stdout]    ✔  And direction ← vector(0, 1, 0)
[INFO] [stdout]    ✔  And r ← ray(origin, direction)
[INFO] [stdout]    ✔  When xs ← local_intersect(p, r)
[INFO] [stdout]    ✔  Then xs.count = 1
[INFO] [stdout]    ✔  And xs[0].t = 1
[INFO] [stdout]    ✔  And xs[0].object = pl
[INFO] [stdout] Feature: Rays
[INFO] [stdout]   Scenario: Creating and querying a ray
[INFO] [stdout]    ✔  Given origin ← point(1, 2, 3)
[INFO] [stdout]    ✔  And direction ← vector(4, 5, 6)
[INFO] [stdout]    ✔  When r ← ray(origin, direction)
[INFO] [stdout]    ✔  Then r.origin = origin
[INFO] [stdout]    ✔  And r.direction = direction
[INFO] [stdout]   Scenario: Computing a point from a distance
[INFO] [stdout]    ✔  Given origin ← point(2, 3, 4)
[INFO] [stdout]    ✔  And direction ← vector(1, 0, 0)
[INFO] [stdout]    ✔  And r ← ray(origin, direction)
[INFO] [stdout]    ✔  Then position(r, 0.0) = point(2, 3, 4)
[INFO] [stdout]    ✔  And position(r, 1.0) = point(3, 3, 4)
[INFO] [stdout]    ✔  And position(r, -1.0) = point(1, 3, 4)
[INFO] [stdout]    ✔  And position(r, 2.5) = point(4.5, 3, 4)
[INFO] [stdout]   Scenario: Translating a ray
[INFO] [stdout]    ✔  Given origin ← point(1, 2, 3)
[INFO] [stdout]    ✔  And direction ← vector(0, 1, 0)
[INFO] [stdout]    ✔  And r ← ray(origin, direction)
[INFO] [stdout]    ✔  And m ← translation(3, 4, 5)
[INFO] [stdout]    ✔  When r2 ← transform(r, m)
[INFO] [stdout]    ✔  Then r2.origin = point(4, 6, 8)
[INFO] [stdout]    ✔  And r2.direction = vector(0, 1, 0)
[INFO] [stdout]   Scenario: Scaling a ray
[INFO] [stdout]    ✔  Given origin ← point(1, 2, 3)
[INFO] [stdout]    ✔  And direction ← vector(0, 1, 0)
[INFO] [stdout]    ✔  And r ← ray(origin, direction)
[INFO] [stdout]    ✔  And m ← scaling(2, 3, 4)
[INFO] [stdout]    ✔  When r2 ← transform(r, m)
[INFO] [stdout]    ✔  Then r2.origin = point(2, 6, 12)
[INFO] [stdout]    ✔  And r2.direction = vector(0, 3, 0)
[INFO] [stdout] Feature: Abstract Shapes
[INFO] [stdout]   Scenario: The default transformation
[INFO] [stdout]    ✔  Given s ← test_shape
[INFO] [stdout]    ✔  Then s.transform = identity_matrix
[INFO] [stdout]   Scenario: Assigning a transformation
[INFO] [stdout]    ✔  Given s ← test_shape
[INFO] [stdout]    ✔  And transform = translation(2, 3, 4)
[INFO] [stdout]    ✔  When set_transform(s, transform)
[INFO] [stdout]    ✔  Then s.transform = translation(2, 3, 4)
[INFO] [stdout]   Scenario: The default material
[INFO] [stdout]    ✔  Given s ← test_shape
[INFO] [stdout]    ✔  When m ← s.material
[INFO] [stdout]    ✔  Then m = material
[INFO] [stdout]   Scenario: Assigning a material
[INFO] [stdout]    ✔  Given s ← test_shape
[INFO] [stdout]    ✔  And m ← material
[INFO] [stdout]    ✔  And m.ambient ← 1
[INFO] [stdout]    ✔  When s.material ← m
[INFO] [stdout]    ✔  Then s.material = m
[INFO] [stdout]   Scenario: Intersecting a scaled shape with a ray
[INFO] [stdout]    ✔  Given origin ← point(0, 0, -5)
[INFO] [stdout]    ✔  And direction ← vector(0, 0, 1)
[INFO] [stdout]    ✔  And r ← ray(origin, direction)
[INFO] [stdout]    ✔  And s ← test_shape
[INFO] [stdout]    ✔  And transform = scaling(2, 2, 2)
[INFO] [stdout]    ✔  When set_transform(s, transform)
[INFO] [stdout]    ✔  And xs ← intersect(s, r)
[INFO] [stdout]    ✔  Then s.saved_ray.origin = point(0, 0, -2.5)
[INFO] [stdout]    ✔  And s.saved_ray.direction = vector(0, 0, 0.5)
[INFO] [stdout]   Scenario: Intersecting a translated shape with a ray
[INFO] [stdout]    ✔  Given origin ← point(0, 0, -5)
[INFO] [stdout]    ✔  And direction ← vector(0, 0, 1)
[INFO] [stdout]    ✔  And r ← ray(origin, direction)
[INFO] [stdout]    ✔  And s ← test_shape
[INFO] [stdout]    ✔  And transform = translation(5, 0, 0)
[INFO] [stdout]    ✔  When set_transform(s, transform)
[INFO] [stdout]    ✔  And xs ← intersect(s, r)
[INFO] [stdout]    ✔  Then s.saved_ray.origin = point(-5, 0, -5)
[INFO] [stdout]    ✔  And s.saved_ray.direction = vector(0, 0, 1)
[INFO] [stdout]   Scenario: Computing the normal on a translated shape
[INFO] [stdout]    ✔  Given s ← test_shape
[INFO] [stdout]    ✔  And transform = translation(0, 1, 0)
[INFO] [stdout]    ✔  When set_transform(s, transform)
[INFO] [stdout]    ✔  And p ← point(0.0, 1.70711, -0.70711)
[INFO] [stdout]    ✔  And n ← normal_at(s, p)
[INFO] [stdout]    ✔  Then n = vector(0.0, 0.70711, -0.70711)
[INFO] [stdout]   Scenario: Computing the normal on a transformed shape
[INFO] [stdout]    ✔  Given s ← test_shape
[INFO] [stdout]    ✔  And transform ← scaling(1, 0.5, 1) * rotation_z(0.62831853071)
[INFO] [stdout]    ✔  When set_transform(s, transform)
[INFO] [stdout]    ✔  And p ← point(0.0, 0.70710678118, -0.70710678118)
[INFO] [stdout]    ✔  And n ← normal_at(s, p)
[INFO] [stdout]    ✔  Then n = vector(0.0, 0.97014, -0.24254)
[INFO] [stdout]   Scenario: A shape has a parent attribute
[INFO] [stdout]    ✔  Given s ← test_shape
[INFO] [stdout]    ✔  Then s.parent is nothing
[INFO] [stdout]   Scenario: Converting a point from world to object space
[INFO] [stdout]    ✔  Given g1 ← group()
[INFO] [stdout]    ✔  And set_transform(g1, rotation_y(1.5707963267948966))
[INFO] [stdout]    ✔  And g2 ← group()
[INFO] [stdout]    ✔  And set_transform(g2, scaling(2, 2, 2))
[INFO] [stdout]    ✔  And add_child(g1, g2)
[INFO] [stdout]    ✔  And s ← sphere()
[INFO] [stdout]    ✔  And set_transform(s, translation(5, 0, 0))
[INFO] [stdout]    ✔  And add_child(g2, s)
[INFO] [stdout]    ✔  When p ← world_to_object(s, point(-2, 0, -10))
[INFO] [stdout]    ✔  Then p = point(0, 0, -1)
[INFO] [stdout] Feature: Smooth Triangles
[INFO] [stdout]   Scenario: Constructing a smooth triangle
[INFO] [stdout]    ✔> Given p1 ← point(0, 1, 0)
[INFO] [stdout]    ✔> And p2 ← point(-1, 0, 0)
[INFO] [stdout]    ✔> And p3 ← point(1, 0, 0)
[INFO] [stdout]    ✔> And n1 ← vector(0, 1, 0)
[INFO] [stdout]    ✔> And n2 ← vector(-1, 0, 0)
[INFO] [stdout]    ✔> And n3 ← vector(1, 0, 0)
[INFO] [stdout]    ✔> When tri ← smooth_triangle(p1, p2, p3, n1, n2, n3)
[INFO] [stdout]    ✔  Then tri.p1 = p1
[INFO] [stdout]    ✔  And tri.p2 = p2
[INFO] [stdout]    ✔  And tri.p3 = p3
[INFO] [stdout]    ✔  And tri.n1 = n1
[INFO] [stdout]    ✔  And tri.n2 = n2
[INFO] [stdout]    ✔  And tri.n3 = n3
[INFO] [stdout]   Scenario: An intersection with a smooth triangle stores u/v
[INFO] [stdout]    ✔> Given p1 ← point(0, 1, 0)
[INFO] [stdout]    ✔> And p2 ← point(-1, 0, 0)
[INFO] [stdout]    ✔> And p3 ← point(1, 0, 0)
[INFO] [stdout]    ✔> And n1 ← vector(0, 1, 0)
[INFO] [stdout]    ✔> And n2 ← vector(-1, 0, 0)
[INFO] [stdout]    ✔> And n3 ← vector(1, 0, 0)
[INFO] [stdout]    ✔> When tri ← smooth_triangle(p1, p2, p3, n1, n2, n3)
[INFO] [stdout]    ✔  When r ← ray(point(-0.2, 0.3, -2), vector(0, 0, 1))
[INFO] [stdout]    ✔  And xs ← local_intersect(tri, r)
[INFO] [stdout]    ✔  Then xs[0].u = 0.45
[INFO] [stdout]    ✔  And xs[0].v = 0.25
[INFO] [stdout]   Scenario: A smooth triangle uses u/v to interpolate the normal
[INFO] [stdout]    ✔> Given p1 ← point(0, 1, 0)
[INFO] [stdout]    ✔> And p2 ← point(-1, 0, 0)
[INFO] [stdout]    ✔> And p3 ← point(1, 0, 0)
[INFO] [stdout]    ✔> And n1 ← vector(0, 1, 0)
[INFO] [stdout]    ✔> And n2 ← vector(-1, 0, 0)
[INFO] [stdout]    ✔> And n3 ← vector(1, 0, 0)
[INFO] [stdout]    ✔> When tri ← smooth_triangle(p1, p2, p3, n1, n2, n3)
[INFO] [stdout]    ✔  When i ← intersection_with_uv(1, tri, 0.45, 0.25)
[INFO] [stdout]    ✔  And n ← normal_at(tri, point(0, 0, 0), i)
[INFO] [stdout]    ✔  Then n = vector(-0.5547, 0.83205, 0)
[INFO] [stdout]   Scenario: Preparing the normal on a smooth triangle
[INFO] [stdout]    ✔> Given p1 ← point(0, 1, 0)
[INFO] [stdout]    ✔> And p2 ← point(-1, 0, 0)
[INFO] [stdout]    ✔> And p3 ← point(1, 0, 0)
[INFO] [stdout]    ✔> And n1 ← vector(0, 1, 0)
[INFO] [stdout]    ✔> And n2 ← vector(-1, 0, 0)
[INFO] [stdout]    ✔> And n3 ← vector(1, 0, 0)
[INFO] [stdout]    ✔> When tri ← smooth_triangle(p1, p2, p3, n1, n2, n3)
[INFO] [stdout]    ✔  When i ← intersection_with_uv(1, tri, 0.45, 0.25)
[INFO] [stdout]    ✔  And r ← ray(point(-0.2, 0.3, -2), vector(0, 0, 1))
[INFO] [stdout]    ✔  And xs ← intersections(i)
[INFO] [stdout]    ✔  And comps ← prepare_computations(i, r, xs)
[INFO] [stdout]    ✔  Then comps.normalv = vector(-0.5547, 0.83205, 0)
[INFO] [stdout] Feature: Spheres
[INFO] [stdout]   Scenario: A ray intersects a sphere at two points and returns the objects
[INFO] [stdout]    ✔  Given origin ← point(0, 0, -5)
[INFO] [stdout]    ✔  And direction ← vector(0, 0, 1)
[INFO] [stdout]    ✔  And r ← ray(origin, direction)
[INFO] [stdout]    ✔  And s ← sphere
[INFO] [stdout]    ✔  When xs ← intersect(s, r)
[INFO] [stdout]    ✔  Then xs.count = 2
[INFO] [stdout]    ✔  And xs[0].t = 4.0
[INFO] [stdout]    ✔  And xs[1].t = 6.0
[INFO] [stdout]    ✔  And xs[0].object = s
[INFO] [stdout]    ✔  And xs[1].object = s
[INFO] [stdout]   Scenario: A ray intersects a sphere at a tangent
[INFO] [stdout]    ✔  Given origin ← point(0, 1, -5)
[INFO] [stdout]    ✔  And direction ← vector(0, 0, 1)
[INFO] [stdout]    ✔  And r ← ray(origin, direction)
[INFO] [stdout]    ✔  And s ← sphere
[INFO] [stdout]    ✔  When xs ← intersect(s, r)
[INFO] [stdout]    ✔  Then xs.count = 2
[INFO] [stdout]    ✔  And xs[0].t = 5.0
[INFO] [stdout]    ✔  And xs[1].t = 5.0
[INFO] [stdout]   Scenario: A ray misses a sphere
[INFO] [stdout]    ✔  Given origin ← point(0, 2, -5)
[INFO] [stdout]    ✔  And direction ← vector(0, 0, 1)
[INFO] [stdout]    ✔  And r ← ray(origin, direction)
[INFO] [stdout]    ✔  And s ← sphere
[INFO] [stdout]    ✔  When xs ← intersect(s, r)
[INFO] [stdout]    ✔  Then xs.count = 0
[INFO] [stdout]   Scenario: A ray originates inside a sphere
[INFO] [stdout]    ✔  Given origin ← point(0, 0, 0)
[INFO] [stdout]    ✔  And direction ← vector(0, 0, 1)
[INFO] [stdout]    ✔  And r ← ray(origin, direction)
[INFO] [stdout]    ✔  And s ← sphere
[INFO] [stdout]    ✔  When xs ← intersect(s, r)
[INFO] [stdout]    ✔  Then xs.count = 2
[INFO] [stdout]    ✔  And xs[0].t = -1.0
[INFO] [stdout]    ✔  And xs[1].t = 1.0
[INFO] [stdout]   Scenario: A sphere is behind a ray
[INFO] [stdout]    ✔  Given origin ← point(0, 0, 5)
[INFO] [stdout]    ✔  And direction ← vector(0, 0, 1)
[INFO] [stdout]    ✔  And r ← ray(origin, direction)
[INFO] [stdout]    ✔  And s ← sphere
[INFO] [stdout]    ✔  When xs ← intersect(s, r)
[INFO] [stdout]    ✔  Then xs.count = 2
[INFO] [stdout]    ✔  And xs[0].t = -6.0
[INFO] [stdout]    ✔  And xs[1].t = -4.0
[INFO] [stdout]   Scenario: A sphere's default transformation
[INFO] [stdout]    ✔  Given s ← sphere
[INFO] [stdout]    ✔  Then s.transform = identity_matrix
[INFO] [stdout]   Scenario: Changing a sphere's transformation
[INFO] [stdout]    ✔  Given s ← sphere
[INFO] [stdout]    ✔  And t ← translation(2, 3, 4)
[INFO] [stdout]    ✔  When setTransform(s, t)
[INFO] [stdout]    ✔  Then s.transform = t
[INFO] [stdout]   Scenario: Intersecting a scaled sphere with a ray
[INFO] [stdout]    ✔  Given origin ← point(0, 0, -5)
[INFO] [stdout]    ✔  And direction ← vector(0, 0, 1)
[INFO] [stdout]    ✔  And r ← ray(origin, direction)
[INFO] [stdout]    ✔  And t ← scaling(2, 2, 2)
[INFO] [stdout]    ✔  When setTransform(s, t)
[INFO] [stdout]    ✔  And xs ← intersect(s, r)
[INFO] [stdout]    ✔  Then xs.count = 2
[INFO] [stdout]    ✔  And xs[0].t = 3
[INFO] [stdout]    ✔  And xs[1].t = 7
[INFO] [stdout]   Scenario: Intersecting a translated sphere with a ray
[INFO] [stdout]    ✔  Given origin ← point(0, 0, -5)
[INFO] [stdout]    ✔  And direction ← vector(0, 0, 1)
[INFO] [stdout]    ✔  And r ← ray(origin, direction)
[INFO] [stdout]    ✔  And t ← translation(5, 0, 0)
[INFO] [stdout]    ✔  When setTransform(s, t)
[INFO] [stdout]    ✔  And xs ← intersect(s, r)
[INFO] [stdout]    ✔  Then xs.count = 0
[INFO] [stdout]   Scenario: The normal on a sphere at a point on the x axis
[INFO] [stdout]    ✔  Given s ← sphere
[INFO] [stdout]    ✔  And p ← point(1, 0, 0)
[INFO] [stdout]    ✔  When n ← normalAt(s, p)
[INFO] [stdout]    ✔  Then n = vector(1, 0, 0)
[INFO] [stdout]   Scenario: The normal on a sphere at a point on the y axis
[INFO] [stdout]    ✔  Given s ← sphere
[INFO] [stdout]    ✔  And p ← point(0, 1, 0)
[INFO] [stdout]    ✔  When n ← normalAt(s, p)
[INFO] [stdout]    ✔  Then n = vector(0, 1, 0)
[INFO] [stdout]   Scenario: The normal on a sphere at a point on the z axis
[INFO] [stdout]    ✔  Given s ← sphere
[INFO] [stdout]    ✔  And p ← point(0, 0, 1)
[INFO] [stdout]    ✔  When n ← normalAt(s, p)
[INFO] [stdout]    ✔  Then n = vector(0, 0, 1)
[INFO] [stdout]   Scenario: The normal on a sphere at a nonaxial point
[INFO] [stdout]    ✔  Given s ← sphere
[INFO] [stdout]    ✔  And p ← point(0.57735026919, 0.57735026919, 0.57735026919)
[INFO] [stdout]    ✔  When n ← normalAt(s, p)
[INFO] [stdout]    ✔  Then n = vector(0.57735026919, 0.57735026919, 0.57735026919)
[INFO] [stdout]   Scenario: The normal is a normalized vector
[INFO] [stdout]    ✔  Given s ← sphere
[INFO] [stdout]    ✔  And p ← point(0.57735026919, 0.57735026919, 0.57735026919)
[INFO] [stdout]    ✔  When n ← normalAt(s, p)
[INFO] [stdout]    ✔  Then n = normalize(n)
[INFO] [stdout]   Scenario: A sphere has a default material
[INFO] [stdout]    ✔  Given s ← sphere
[INFO] [stdout]    ✔  When m ← s.material
[INFO] [stdout]    ✔  Then m = material
[INFO] [stdout]   Scenario: A sphere may be assigned a material
[INFO] [stdout]    ✔  Given s ← sphere
[INFO] [stdout]    ✔  And m ← material
[INFO] [stdout]    ✔  And m.ambient ← 1
[INFO] [stdout]    ✔  When s.material ← m
[INFO] [stdout]    ✔  Then s.material = m
[INFO] [stdout]   Scenario: A helper for producing a sphere with a glassy material
[INFO] [stdout]    ✔  Given s ← glass_sphere()
[INFO] [stdout]    ✔  Then s.transform = identity_matrix
[INFO] [stdout]    ✔  And s.material.transparency = 1.0
[INFO] [stdout]    ✔  And s.material.refractive_index = 1.5
[INFO] [stdout] Feature: Matrix Transformations
[INFO] [stdout]   Scenario: Multiplying by a translation matrix
[INFO] [stdout]    ✔  Given transform ← translation(5, -3, 2)
[INFO] [stdout]    ✔  And p ← point(-3, 4, 5)
[INFO] [stdout]    ✔  Then transform * p = point(2, 1, 7)
[INFO] [stdout]   Scenario: Multiplying by the inverse of a translation matrix
[INFO] [stdout]    ✔  Given transform ← translation(5, -3, 2)
[INFO] [stdout]    ✔  And inv ← inverse(transform)
[INFO] [stdout]    ✔  And p ← point(-3, 4, 5)
[INFO] [stdout]    ✔  Then inv * p = point(-8, 7, 3)
[INFO] [stdout]   Scenario: Translation does not affect vectors
[INFO] [stdout]    ✔  Given transform ← translation(5, -3, 2)
[INFO] [stdout]    ✔  And v ← vector(-3, 4, 5)
[INFO] [stdout]    ✔  Then transform * v = v
[INFO] [stdout]   Scenario: A scaling matrix applied to a point
[INFO] [stdout]    ✔  Given transform ← scaling(2, 3, 4)
[INFO] [stdout]    ✔  And p ← point(-4, 6, 8)
[INFO] [stdout]    ✔  Then transform * p = point(-8, 18, 32)
[INFO] [stdout]   Scenario: A scaling matrix applied to a vector
[INFO] [stdout]    ✔  Given transform ← scaling(2, 3, 4)
[INFO] [stdout]    ✔  And v ← vector(-4, 6, 8)
[INFO] [stdout]    ✔  Then transform * v = vector(-8, 18, 32)
[INFO] [stdout]   Scenario: Multiplying by the inverse of a scaling matrix
[INFO] [stdout]    ✔  Given transform ← scaling(2, 3, 4)
[INFO] [stdout]    ✔  And inv ← inverse(transform)
[INFO] [stdout]    ✔  And v ← vector(-4, 6, 8)
[INFO] [stdout]    ✔  Then inv * v = vector(-2, 2, 2)
[INFO] [stdout]   Scenario: Reflection is scaling by a negative value
[INFO] [stdout]    ✔  Given transform ← scaling(-1, 1, 1)
[INFO] [stdout]    ✔  And p ← point(2, 3, 4)
[INFO] [stdout]    ✔  Then transform * p = point(-2, 3, 4)
[INFO] [stdout]   Scenario: Rotating a point around the x axis
[INFO] [stdout]    ✔  Given p ← point(0, 1, 0)
[INFO] [stdout]    ✔  And half_quarter ← rotation_x(π / 4)
[INFO] [stdout]    ✔  And full_quarter ← rotation_x(π / 2)
[INFO] [stdout]    ✔  Then half_quarter * p = point(0.0, 0.7071, 0.7071)
[INFO] [stdout]    ✔  And full_quarter * p = point(0, 0, 1)
[INFO] [stdout]   Scenario: The inverse of an x-rotation rotates in the opposite direction
[INFO] [stdout]    ✔  Given p ← point(0, 1, 0)
[INFO] [stdout]    ✔  And half_quarter ← rotation_x(π / 4)
[INFO] [stdout]    ✔  And inv ← inverse(half_quarter)
[INFO] [stdout]    ✔  Then inv * p = point(0.0, 0.7071, -0.7071)
[INFO] [stdout]   Scenario: Rotating a point around the y axis
[INFO] [stdout]    ✔  Given p ← point(0, 0, 1)
[INFO] [stdout]    ✔  And half_quarter ← rotation_y(π / 4)
[INFO] [stdout]    ✔  And full_quarter ← rotation_y(π / 2)
[INFO] [stdout]    ✔  Then half_quarter * p = point(0.7071, 0.0, 0.7071)
[INFO] [stdout]    ✔  And full_quarter * p = point(1, 0, 0)
[INFO] [stdout]   Scenario: Rotating a point around the z axis
[INFO] [stdout]    ✔  Given p ← point(0, 1, 0)
[INFO] [stdout]    ✔  And half_quarter ← rotation_z(π / 4)
[INFO] [stdout]    ✔  And full_quarter ← rotation_z(π / 2)
[INFO] [stdout]    ✔  Then half_quarter * p = point(-0.7071, 0.7071, 0.0)
[INFO] [stdout]    ✔  And full_quarter * p = point(-1, 0, 0)
[INFO] [stdout]   Scenario: A shearing transformation moves x in proportion to y
[INFO] [stdout]    ✔  Given transform ← shearing(1, 0, 0, 0, 0, 0)
[INFO] [stdout]    ✔  And p ← point(2, 3, 4)
[INFO] [stdout]    ✔  Then transform * p = point(5, 3, 4)
[INFO] [stdout]   Scenario: A shearing transformation moves x in proportion to z
[INFO] [stdout]    ✔  Given transform ← shearing(0, 1, 0, 0, 0, 0)
[INFO] [stdout]    ✔  And p ← point(2, 3, 4)
[INFO] [stdout]    ✔  Then transform * p = point(6, 3, 4)
[INFO] [stdout]   Scenario: A shearing transformation moves y in proportion to x
[INFO] [stdout]    ✔  Given transform ← shearing(0, 0, 1, 0, 0, 0)
[INFO] [stdout]    ✔  And p ← point(2, 3, 4)
[INFO] [stdout]    ✔  Then transform * p = point(2, 5, 4)
[INFO] [stdout]   Scenario: A shearing transformation moves y in proportion to z
[INFO] [stdout]    ✔  Given transform ← shearing(0, 0, 0, 1, 0, 0)
[INFO] [stdout]    ✔  And p ← point(2, 3, 4)
[INFO] [stdout]    ✔  Then transform * p = point(2, 7, 4)
[INFO] [stdout]   Scenario: A shearing transformation moves z in proportion to x
[INFO] [stdout]    ✔  Given transform ← shearing(0, 0, 0, 0, 1, 0)
[INFO] [stdout]    ✔  And p ← point(2, 3, 4)
[INFO] [stdout]    ✔  Then transform * p = point(2, 3, 6)
[INFO] [stdout]   Scenario: A shearing transformation moves z in proportion to y
[INFO] [stdout]    ✔  Given transform ← shearing(0, 0, 0, 0, 0, 1)
[INFO] [stdout]    ✔  And p ← point(2, 3, 4)
[INFO] [stdout]    ✔  Then transform * p = point(2, 3, 7)
[INFO] [stdout]   Scenario: Individual transformations are applied in sequence
[INFO] [stdout]    ✔  Given p ← point(1, 0, 1)
[INFO] [stdout]    ✔  And A ← rotation_x(π / 2)
[INFO] [stdout]    ✔  And B ← scaling(5, 5, 5)
[INFO] [stdout]    ✔  And C ← translation(10, 5, 7)
[INFO] [stdout]    ✔  When p2 ← A * p
[INFO] [stdout]    ✔  Then p2 = point(1, -1, 0)
[INFO] [stdout]    ✔  When p3 ← B * p2
[INFO] [stdout]    ✔  Then p3 = point(5, -5, 0)
[INFO] [stdout]    ✔  When p4 ← C * p3
[INFO] [stdout]    ✔  Then p4 = point(15, 0, 7)
[INFO] [stdout]   Scenario: Chained transformations must be applied in reverse order
[INFO] [stdout]    ✔  Given p ← point(1, 0, 1)
[INFO] [stdout]    ✔  And A ← rotation_x(π / 2)
[INFO] [stdout]    ✔  And B ← scaling(5, 5, 5)
[INFO] [stdout]    ✔  And C ← translation(10, 5, 7)
[INFO] [stdout]    ✔  When T ← C * B * A
[INFO] [stdout]    ✔  Then T * p = point(15, 0, 7)
[INFO] [stdout]   Scenario: The transformation matrix for the default orientation
[INFO] [stdout]    ✔  Given from ← point(0, 0, 0)
[INFO] [stdout]    ✔  And to ← point(0, 0, -1)
[INFO] [stdout]    ✔  And up ← vector(0, 1, 0)
[INFO] [stdout]    ✔  When t ← view_transform(from, to, up)
[INFO] [stdout]    ✔  Then t = identity_matrix
[INFO] [stdout]   Scenario: A view transformation matrix looking in positive z direction
[INFO] [stdout]    ✔  Given from ← point(0, 0, 0)
[INFO] [stdout]    ✔  And to ← point(0, 0, 1)
[INFO] [stdout]    ✔  And up ← vector(0, 1, 0)
[INFO] [stdout]    ✔  When t ← view_transform(from, to, up)
[INFO] [stdout]    ✔  Then t = scaling(-1, 1, -1)
[INFO] [stdout]   Scenario: The view transformation moves the world
[INFO] [stdout]    ✔  Given from ← point(0, 0, 8)
[INFO] [stdout]    ✔  And to ← point(0, 0, 0)
[INFO] [stdout]    ✔  And up ← vector(0, 1, 0)
[INFO] [stdout]    ✔  When t ← view_transform(from, to, up)
[INFO] [stdout]    ✔  Then t = translation(0, 0, -8)
[INFO] [stdout]   Scenario: An arbitrary view transformation
[INFO] [stdout]    ✔  Given from ← point(1, 3, 2)
[INFO] [stdout]    ✔  And to ← point(4, -2, 8)
[INFO] [stdout]    ✔  And up ← vector(1, 1, 0)
[INFO] [stdout]    ✔  When t ← view_transform(from, to, up)
[INFO] [stdout]    ✔  Then t is the following 4x4 matrix:
[INFO] [stdout]        | -0.50709 | 0.50709 | 0.67612  | -2.36643 |
[INFO] [stdout]        | 0.76772  | 0.60609 | 0.12122  | -2.82843 |
[INFO] [stdout]        | -0.35857 | 0.59761 | -0.71714 | 0.00000  |
[INFO] [stdout]        | 0.00000  | 0.00000 | 0.00000  | 1.00000  |
[INFO] [stdout] Feature: Triangles
[INFO] [stdout]   Scenario: Constructing a triangle
[INFO] [stdout]    ✔  Given p1 ← point(0, 1, 0)
[INFO] [stdout]    ✔  And p2 ← point(-1, 0, 0)
[INFO] [stdout]    ✔  And p3 ← point(1, 0, 0)
[INFO] [stdout]    ✔  And t ← triangle(p1, p2, p3)
[INFO] [stdout]    ✔  Then t.p1 = p1
[INFO] [stdout]    ✔  And t.p2 = p2
[INFO] [stdout]    ✔  And t.p3 = p3
[INFO] [stdout]    ✔  And t.e1 = vector(-1, -1, 0)
[INFO] [stdout]    ✔  And t.e2 = vector(1, -1, 0)
[INFO] [stdout]    ✔  And t.normal = vector(0, 0, -1)
[INFO] [stdout]   Scenario: Intersecting a ray parallel to the triangle
[INFO] [stdout]    ✔  Given t ← triangle(point(0, 1, 0), point(-1, 0, 0), point(1, 0, 0))
[INFO] [stdout]    ✔  And r ← ray(point(0, -1, -2), vector(0, 1, 0))
[INFO] [stdout]    ✔  When xs ← local_intersect(t, r)
[INFO] [stdout]    ✔  Then xs is empty
[INFO] [stdout]   Scenario: A ray misses the p1-p3 edge
[INFO] [stdout]    ✔  Given t ← triangle(point(0, 1, 0), point(-1, 0, 0), point(1, 0, 0))
[INFO] [stdout]    ✔  And r ← ray(point(1, 1, -2), vector(0, 0, 1))
[INFO] [stdout]    ✔  When xs ← local_intersect(t, r)
[INFO] [stdout]    ✔  Then xs is empty
[INFO] [stdout]   Scenario: A ray misses the p1-p2 edge
[INFO] [stdout]    ✔  Given t ← triangle(point(0, 1, 0), point(-1, 0, 0), point(1, 0, 0))
[INFO] [stdout]    ✔  And r ← ray(point(-1, 1, -2), vector(0, 0, 1))
[INFO] [stdout]    ✔  When xs ← local_intersect(t, r)
[INFO] [stdout]    ✔  Then xs is empty
[INFO] [stdout]   Scenario: A ray misses the p2-p3 edge
[INFO] [stdout]    ✔  Given t ← triangle(point(0, 1, 0), point(-1, 0, 0), point(1, 0, 0))
[INFO] [stdout]    ✔  And r ← ray(point(0, -1, -2), vector(0, 0, 1))
[INFO] [stdout]    ✔  When xs ← local_intersect(t, r)
[INFO] [stdout]    ✔  Then xs is empty
[INFO] [stdout]   Scenario: A ray strikes a triangle
[INFO] [stdout]    ✔  Given t ← triangle(point(0, 1, 0), point(-1, 0, 0), point(1, 0, 0))
[INFO] [stdout]    ✔  And r ← ray(point(0, 0.5, -2), vector(0, 0, 1))
[INFO] [stdout]    ✔  When xs ← local_intersect(t, r)
[INFO] [stdout]    ✔  Then xs.count = 1
[INFO] [stdout]    ✔  And xs[0].t = 2
[INFO] [stdout]   Scenario: Finding the normal on a triangle
[INFO] [stdout]    ✔  Given t ← triangle(point(0, 1, 0), point(-1, 0, 0), point(1, 0, 0))
[INFO] [stdout]    ✔  When n1 ← local_normal_at(t, point(0, 0.5, 0))
[INFO] [stdout]    ✔  And n2 ← local_normal_at(t, point(-0.5, 0.75, 0))
[INFO] [stdout]    ✔  And n3 ← local_normal_at(t, point(0.5, 0.25, 0))
[INFO] [stdout]    ✔  Then n1 = t.normal
[INFO] [stdout]    ✔  And n2 = t.normal
[INFO] [stdout]    ✔  And n3 = t.normal
[INFO] [stdout] Feature: Tuples, Vectors, and Points
[INFO] [stdout]   Scenario: A tuple with w=1.0 is a point
[INFO] [stdout]    ✔  Given a ← tuple(4.3, -4.2, 3.1, 1.0)
[INFO] [stdout]    ✔  Then a.x = 4.3
[INFO] [stdout]    ✔  And a.y = -4.2
[INFO] [stdout]    ✔  And a.z = 3.1
[INFO] [stdout]    ✔  And a.w = 1.0
[INFO] [stdout]    ✔  And a is a point
[INFO] [stdout]    ✔  And a is not a vector
[INFO] [stdout]   Scenario: A tuple with w=0 is a vector
[INFO] [stdout]    ✔  Given a ← tuple(4.3, -4.2, 3.1, 0.0)
[INFO] [stdout]    ✔  Then a.x = 4.3
[INFO] [stdout]    ✔  And a.y = -4.2
[INFO] [stdout]    ✔  And a.z = 3.1
[INFO] [stdout]    ✔  And a.w = 0.0
[INFO] [stdout]    ✔  And a is not a point
[INFO] [stdout]    ✔  And a is a vector
[INFO] [stdout]   Scenario: point() creates tuples with w=1
[INFO] [stdout]    ✔  Given p ← point(4, -4, 3)
[INFO] [stdout]    ✔  Then p = tuple(4, -4, 3, 1)
[INFO] [stdout]   Scenario: vector() creates tuples with w=0
[INFO] [stdout]    ✔  Given v ← vector(4, -4, 3)
[INFO] [stdout]    ✔  Then v = tuple(4, -4, 3, 0)
[INFO] [stdout]   Scenario: Adding two tuples
[INFO] [stdout]    ✔  Given a1 ← tuple(3, -2, 5, 1)
[INFO] [stdout]    ✔  And a2 ← tuple(-2, 3, 1, 0)
[INFO] [stdout]    ✔  Then a1 + a2 = tuple(1, 1, 6, 1)
[INFO] [stdout]   Scenario: Subtracting two points
[INFO] [stdout]    ✔  Given p1 ← point(3, 2, 1)
[INFO] [stdout]    ✔  And p2 ← point(5, 6, 7)
[INFO] [stdout]    ✔  Then p1 - p2 = vector(-2, -4, -6)
[INFO] [stdout]   Scenario: Subtracting a vector from a point
[INFO] [stdout]    ✔  Given p ← point(3, 2, 1)
[INFO] [stdout]    ✔  And v ← vector(5, 6, 7)
[INFO] [stdout]    ✔  Then p - v = point(-2, -4, -6)
[INFO] [stdout]   Scenario: Subtracting two vectors
[INFO] [stdout]    ✔  Given v1 ← vector(3, 2, 1)
[INFO] [stdout]    ✔  And v2 ← vector(5, 6, 7)
[INFO] [stdout]    ✔  Then v1 - v2 = vector(-2, -4, -6)
[INFO] [stdout]   Scenario: Subtracting a vector from the zero vector
[INFO] [stdout]    ✔  Given zero ← vector(0, 0, 0)
[INFO] [stdout]    ✔  And v ← vector(1, -2, 3)
[INFO] [stdout]    ✔  Then zero - v = vector(-1, 2, -3)
[INFO] [stdout]   Scenario: Negating a tuple
[INFO] [stdout]    ✔  Given a ← tuple(1, -2, 3, -4)
[INFO] [stdout]    ✔  Then -a = tuple(-1, 2, -3, 4)
[INFO] [stdout]   Scenario: Multiplying a tuple by a scalar
[INFO] [stdout]    ✔  Given a ← tuple(1, -2, 3, -4)
[INFO] [stdout]    ✔  Then a * 3.5 = tuple(3.5, -7.0, 10.5, -14.0)
[INFO] [stdout]   Scenario: Multiplying a tuple by a fraction
[INFO] [stdout]    ✔  Given a ← tuple(1, -2, 3, -4)
[INFO] [stdout]    ✔  Then a * 0.5 = tuple(0.5, -1.0, 1.5, -2.0)
[INFO] [stdout]   Scenario: Dividing a tuple by a scalar
[INFO] [stdout]    ✔  Given ad ← tuple(1, -2, 3, -4)
[INFO] [stdout]    ✔  Then ad / 2.0 = tuple(0.5, -1.0, 1.5, -2.0)
[INFO] [stdout]   Scenario: Computing the magnitude of vector(1, 0, 0)
[INFO] [stdout]    ✔  Given v2 ← vector(1, 0, 0)
[INFO] [stdout]    ✔  Then magnitude(v2) = 1
[INFO] [stdout]   Scenario: Computing the magnitude of vector(0, 1, 0)
[INFO] [stdout]    ✔  Given v2 ← vector(0, 1, 0)
[INFO] [stdout]    ✔  Then magnitude(v2) = 1
[INFO] [stdout]   Scenario: Computing the magnitude of vector(0, 0, 1)
[INFO] [stdout]    ✔  Given v2 ← vector(0, 0, 1)
[INFO] [stdout]    ✔  Then magnitude(v2) = 1
[INFO] [stdout]   Scenario: Computing the magnitude of vector(1, 2, 3)
[INFO] [stdout]    ✔  Given v ← vector(1, 2, 3)
[INFO] [stdout]    ✔  Then magnitude(v) = √14
[INFO] [stdout]   Scenario: Computing the magnitude of vector(-1, -2, -3)
[INFO] [stdout]    ✔  Given v ← vector(-1, -2, -3)
[INFO] [stdout]    ✔  Then magnitude(v) = √14
[INFO] [stdout]   Scenario: Normalizing vector(4, 0, 0) gives (1, 0, 0)
[INFO] [stdout]    ✔  Given v ← vector(4, 0, 0)
[INFO] [stdout]    ✔  Then normalize(v) = vector(1, 0, 0)
[INFO] [stdout]   Scenario: Normalizing vector(1, 2, 3)
[INFO] [stdout]    ✔  Given v ← vector(1, 2, 3)
[INFO] [stdout]    ✔  Then normalize(v) = approximately vector(0.26726, 0.53452, 0.80178)
[INFO] [stdout]   Scenario: The magnitude of a normalized vector
[INFO] [stdout]    ✔  Given v ← vector(1, 2, 3)
[INFO] [stdout]    ✔  When norm ← normalize(v)
[INFO] [stdout]    ✔  Then magnitude(norm) = 1
[INFO] [stdout]   Scenario: The dot product of two tuples
[INFO] [stdout]    ✔  Given a ← vector(1, 2, 3)
[INFO] [stdout]    ✔  And b ← vector(2, 3, 4)
[INFO] [stdout]    ✔  Then dot(a, b) = 20
[INFO] [stdout]   Scenario: The cross product of two vectors
[INFO] [stdout]    ✔  Given a ← vector(1, 2, 3)
[INFO] [stdout]    ✔  And b ← vector(2, 3, 4)
[INFO] [stdout]    ✔  Then cross(a, b) = vector(-1, 2, -1)
[INFO] [stdout]    ✔  And cross(b, a) = vector(1, -2, 1)
[INFO] [stdout]   Scenario: Colors are (red, green, blue) tuples
[INFO] [stdout]    ✔  Given c ← color(-0.5, 0.4, 1.7)
[INFO] [stdout]    ✔  Then c.red = -0.5
[INFO] [stdout]    ✔  And c.green = 0.4
[INFO] [stdout]    ✔  And c.blue = 1.7
[INFO] [stdout]   Scenario: Adding colors
[INFO] [stdout]    ✔  Given c1 ← color(0.9, 0.6, 0.75)
[INFO] [stdout]    ✔  And c2 ← color(0.7, 0.1, 0.25)
[INFO] [stdout]    ✔  Then c1 + c2 = color(1.6, 0.7, 1.0)
[INFO] [stdout]   Scenario: Subtracting colors
[INFO] [stdout]    ✔  Given c1 ← color(0.9, 0.6, 0.75)
[INFO] [stdout]    ✔  And c3 ← color(0.7, 0.1, 0.25)
[INFO] [stdout]    ✔  Then c1 - c3 = color(0.2, 0.5, 0.5)
[INFO] [stdout]   Scenario: Multiplying a color by a scalar
[INFO] [stdout]    ✔  Given c ← color(0.2, 0.3, 0.4)
[INFO] [stdout]    ✔  Then c * 2.0 = color(0.4, 0.6, 0.8)
[INFO] [stdout]   Scenario: Multiplying colors
[INFO] [stdout]    ✔  Given c1 ← color(1.0, 0.2, 0.4)
[INFO] [stdout]    ✔  And c4 ← color(0.9, 1.0, 0.1)
[INFO] [stdout]    ✔  Then c1 * c4 = color(0.9, 0.2, 0.04)
[INFO] [stdout]   Scenario: Reflecting a vector approaching at 45°
[INFO] [stdout]    ✔  Given v ← vector(1, -1, 0)
[INFO] [stdout]    ✔  And n ← vector(0.0, 1.0, 0.0)
[INFO] [stdout]    ✔  When r ← reflect(v, n)
[INFO] [stdout]    ✔  Then r = vector(1, 1, 0)
[INFO] [stdout]   Scenario: Reflecting a vector off a slanted surface
[INFO] [stdout]    ✔  Given v ← vector(0, -1, 0)
[INFO] [stdout]    ✔  And n ← vector(0.70710678118, 0.70710678118, 0.0)
[INFO] [stdout]    ✔  When r ← reflect(v, n)
[INFO] [stdout]    ✔  Then r = vector(1, 0, 0)
[INFO] [stdout] Feature: World
[INFO] [stdout]   Scenario: Creating a world
[INFO] [stdout]    ✔  Given w ← world
[INFO] [stdout]    ✔  Then w contains no objects
[INFO] [stdout]    ✔  And w has no light source
[INFO] [stdout]   Scenario: The default world
[INFO] [stdout]    ✔  Given pos ← point(-10, 10, -10)
[INFO] [stdout]    ✔  And intensity ← color(1, 1, 1)
[INFO] [stdout]    ✔  And light ← point_light(pos, intensity)
[INFO] [stdout]    ✔  And s1 ← sphere with:
[INFO] [stdout]        | material.color    | (0.8, 1.0, 0.6) |
[INFO] [stdout]        | material.diffuse  | 0.7             |
[INFO] [stdout]        | material.specular | 0.2             |
[INFO] [stdout]    ✔  And s2 ← sphere with:
[INFO] [stdout]        | transform | scaling(0.5, 0.5, 0.5) |
[INFO] [stdout]    ✔  When w ← defaultWorld
[INFO] [stdout]    ✔  Then w.light = light
[INFO] [stdout]    ✔  And w contains s1
[INFO] [stdout]    ✔  And w contains s2
[INFO] [stdout]   Scenario: Intersect a world with a ray
[INFO] [stdout]    ✔  Given w ← defaultWorld
[INFO] [stdout]    ✔  And origin ← point(0, 0, -5)
[INFO] [stdout]    ✔  And direction ← vector(0, 0, 1)
[INFO] [stdout]    ✔  And r ← ray(origin, direction)
[INFO] [stdout]    ✔  When xs ← intersect_world(w, r)
[INFO] [stdout]    ✔  Then xs.count = 4
[INFO] [stdout]    ✔  And xs[0].t = 4
[INFO] [stdout]    ✔  And xs[1].t = 4.5
[INFO] [stdout]    ✔  And xs[2].t = 5.5
[INFO] [stdout]    ✔  And xs[3].t = 6
[INFO] [stdout]   Scenario: Shading an intersection
[INFO] [stdout]    ✔  Given w ← defaultWorld
[INFO] [stdout]    ✔  And origin ← point(0, 0, -5)
[INFO] [stdout]    ✔  And direction ← vector(0, 0, 1)
[INFO] [stdout]    ✔  And r ← ray(origin, direction)
[INFO] [stdout]    ✔  And shape ← the first object in w
[INFO] [stdout]    ✔  And i ← intersection(4, shape)
[INFO] [stdout]    ✔  When comps ← prepare_computations(i, r)
[INFO] [stdout]    ✔  And c ← shade_hit(w, comps)
[INFO] [stdout]    ✔  Then c = color(0.38066, 0.47583, 0.2855)
[INFO] [stdout]   Scenario: Shading an intersection from the inside
[INFO] [stdout]    ✔  Given w ← defaultWorld
[INFO] [stdout]    ✔  And position ← point(0.0, 0.25, 0.0)
[INFO] [stdout]    ✔  And intensity ← color(1, 1, 1)
[INFO] [stdout]    ✔  And w.light ← point_light(position, intensity)
[INFO] [stdout]    ✔  And origin ← point(0, 0, 0)
[INFO] [stdout]    ✔  And direction ← vector(0, 0, 1)
[INFO] [stdout]    ✔  And r ← ray(origin, direction)
[INFO] [stdout]    ✔  And shape ← the second object in w
[INFO] [stdout]    ✔  And i ← intersection(0.5, shape)
[INFO] [stdout]    ✔  When comps ← prepare_computations(i, r)
[INFO] [stdout]    ✔  And c ← shade_hit(w, comps)
[INFO] [stdout]    ✔  Then c = color(0.90498, 0.90498, 0.90498)
[INFO] [stdout]   Scenario: The color when a ray misses
[INFO] [stdout]    ✔  Given w ← defaultWorld
[INFO] [stdout]    ✔  And origin ← point(0, 0, -5)
[INFO] [stdout]    ✔  And direction ← vector(0, 1, 0)
[INFO] [stdout]    ✔  And r ← ray(origin, direction)
[INFO] [stdout]    ✔  When c ← color_at(w, r)
[INFO] [stdout]    ✔  Then c = color(0, 0, 0)
[INFO] [stdout]   Scenario: The color when a ray hits
[INFO] [stdout]    ✔  Given w ← defaultWorld
[INFO] [stdout]    ✔  And origin ← point(0, 0, -5)
[INFO] [stdout]    ✔  And direction ← vector(0, 0, 1)
[INFO] [stdout]    ✔  And r ← ray(origin, direction)
[INFO] [stdout]    ✔  When c ← color_at(w, r)
[INFO] [stdout]    ✔  Then c = color(0.38066, 0.47583, 0.2855)
[INFO] [stdout]   Scenario: The color with an intersection behind the ray
[INFO] [stdout]    ✔  Given w ← defaultWorld
[INFO] [stdout]    ✔  And outer ← the first object in w
[INFO] [stdout]    ✔  And outer.material.ambient ← 1
[INFO] [stdout]    ✔  And inner ← the second object in w
[INFO] [stdout]    ✔  And inner.material.ambient ← 1
[INFO] [stdout]    ✔  And origin ← point(0.0, 0.0, 0.75)
[INFO] [stdout]    ✔  And direction ← vector(0, 0, -1)
[INFO] [stdout]    ✔  And r ← ray(origin, direction)
[INFO] [stdout]    ✔  When c ← color_at(w, r)
[INFO] [stdout]    ✔  Then c = inner.material.color
[INFO] [stdout]   Scenario: There is no shadow when nothing is collinear with point and light
[INFO] [stdout]    ✔  Given w ← defaultWorld
[INFO] [stdout]    ✔  And p ← point(0, 10, 0)
[INFO] [stdout]    ✔  Then is_shadowed(w, p) is false
[INFO] [stdout]   Scenario: The shadow when an object is between the point and the light
[INFO] [stdout]    ✔  Given w ← defaultWorld
[INFO] [stdout]    ✔  And p ← point(10, -10, 10)
[INFO] [stdout]    ✔  Then is_shadowed(w, p) is true
[INFO] [stdout]   Scenario: There is no shadow when an object is behind the light
[INFO] [stdout]    ✔  Given w ← defaultWorld
[INFO] [stdout]    ✔  And p ← point(-20, 20, -20)
[INFO] [stdout]    ✔  Then is_shadowed(w, p) is false
[INFO] [stdout]   Scenario: There is no shadow when an object is behind the point
[INFO] [stdout]    ✔  Given w ← defaultWorld
[INFO] [stdout]    ✔  And p ← point(-2, 2, -2)
[INFO] [stdout]    ✔  Then is_shadowed(w, p) is false
[INFO] [stdout]   Scenario: shade_hit() is given an intersection in shadow
[INFO] [stdout]    ✔  Given w ← world
[INFO] [stdout]    ✔  And position ← point(0, 0, -10)
[INFO] [stdout]    ✔  And intensity ← color(1, 1, 1)
[INFO] [stdout]    ✔  And w.light ← point_light(position, intensity)
[INFO] [stdout]    ✔  And s1 ← sphere
[INFO] [stdout]    ✔  And s1 is added to w
[INFO] [stdout]    ✔  And s2 ← sphere with:
[INFO] [stdout]        | transform | translation(0, 0, 10) |
[INFO] [stdout]    ✔  And s2 is added to w
[INFO] [stdout]    ✔  And origin ← point(0, 0, 5)
[INFO] [stdout]    ✔  And direction ← vector(0, 0, 1)
[INFO] [stdout]    ✔  And r ← ray(origin, direction)
[INFO] [stdout]    ✔  And i ← intersection(4, s2)
[INFO] [stdout]    ✔  When comps ← prepare_computations(i, r)
[INFO] [stdout]    ✔  And c ← shade_hit(w, comps)
[INFO] [stdout]    ✔  Then c = color(0.1, 0.1, 0.1)
[INFO] [stdout]   Scenario: The reflected color for a nonreflective material
[INFO] [stdout]    ✔  Given w ← defaultWorld
[INFO] [stdout]    ✔  And origin ← point(0, 0, 0)
[INFO] [stdout]    ✔  And direction ← vector(0, 0, 1)
[INFO] [stdout]    ✔  And r ← ray(origin, direction)
[INFO] [stdout]    ✔  And shape ← the second object in w
[INFO] [stdout]    ✔  And shape.material.ambient ← 1
[INFO] [stdout]    ✔  And i ← intersection(1, shape)
[INFO] [stdout]    ✔  When comps ← prepare_computations(i, r)
[INFO] [stdout]    ✔  And color ← reflected_color(w, comps)
[INFO] [stdout]    ✔  Then color = color(0, 0, 0)
[INFO] [stdout]   Scenario: The reflected color for a reflective material
[INFO] [stdout]    ✔  Given w ← defaultWorld
[INFO] [stdout]    ✔  And shape ← plane with:
[INFO] [stdout]        | material.reflective | 0.5                   |
[INFO] [stdout]        | transform           | translation(0, -1, 0) |
[INFO] [stdout]    ✔  And shape is added to w
[INFO] [stdout]    ✔  And origin ← point(0, 0, -3)
[INFO] [stdout]    ✔  And direction ← vector(0.0, -0.70710678118, 0.70710678118)
[INFO] [stdout]    ✔  And r ← ray(origin, direction)
[INFO] [stdout]    ✔  And i ← intersection(1.41421356237, shape)
[INFO] [stdout]    ✔  When comps ← prepare_computations(i, r)
[INFO] [stdout]    ✔  And color ← reflected_color(w, comps)
[INFO] [stdout]    ✔  Then color = color(0.19033, 0.23792, 0.14275)
[INFO] [stdout]   Scenario: shade_hit() with a reflective material
[INFO] [stdout]    ✔  Given w ← defaultWorld
[INFO] [stdout]    ✔  And shape ← plane with:
[INFO] [stdout]        | material.reflective | 0.5                   |
[INFO] [stdout]        | transform           | translation(0, -1, 0) |
[INFO] [stdout]    ✔  And shape is added to w
[INFO] [stdout]    ✔  And origin ← point(0, 0, -3)
[INFO] [stdout]    ✔  And direction ← vector(0.0, -0.70710678118, 0.70710678118)
[INFO] [stdout]    ✔  And r ← ray(origin, direction)
[INFO] [stdout]    ✔  And i ← intersection(1.41421356237, shape)
[INFO] [stdout]    ✔  When comps ← prepare_computations(i, r)
[INFO] [stdout]    ✔  And color ← shade_hit(w, comps)
[INFO] [stdout]    ✔  Then color = color(0.876757, 0.924340, 0.829174)
[INFO] [stdout]   Scenario: color_at() with mutually reflective surfaces
[INFO] [stdout]    ✔  Given w ← world
[INFO] [stdout]    ✔  And position ← point(0, 0, 0)
[INFO] [stdout]    ✔  And intensity ← color(1, 1, 1)
[INFO] [stdout]    ✔  And w.light ← point_light(position, intensity)
[INFO] [stdout]    ✔  And lower ← plane with:
[INFO] [stdout]        | material.reflective | 1                     |
[INFO] [stdout]        | transform           | translation(0, -1, 0) |
[INFO] [stdout]    ✔  And lower is added to w
[INFO] [stdout]    ✔  And upper ← plane with:
[INFO] [stdout]        | material.reflective | 1                    |
[INFO] [stdout]        | transform           | translation(0, 1, 0) |
[INFO] [stdout]    ✔  And upper is added to w
[INFO] [stdout]    ✔  And origin ← point(0, 0, 0)
[INFO] [stdout]    ✔  And direction ← vector(0, 1, 0)
[INFO] [stdout]    ✔  And r ← ray(origin, direction)
[INFO] [stdout]    ✔  Then color_at(w, r) should terminate successfully
[INFO] [stdout]   Scenario: The reflected color at the maximum recursive depth
[INFO] [stdout]    ✔  Given w ← defaultWorld
[INFO] [stdout]    ✔  And shape ← plane with:
[INFO] [stdout]        | material.reflective | 0.5                   |
[INFO] [stdout]        | transform           | translation(0, -1, 0) |
[INFO] [stdout]    ✔  And shape is added to w
[INFO] [stdout]    ✔  And origin ← point(0, 0, -3)
[INFO] [stdout]    ✔  And direction ← vector(0.0, -0.70710678118, 0.70710678118)
[INFO] [stdout]    ✔  And r ← ray(origin, direction)
[INFO] [stdout]    ✔  And i ← intersection(1.41421356237, shape)
[INFO] [stdout]    ✔  When comps ← prepare_computations(i, r)
[INFO] [stdout]    ✔  And color ← reflected_color(w, comps, 0)
[INFO] [stdout]    ✔  Then color = color(0, 0, 0)
[INFO] [stdout]   Scenario: The refracted color with an opaque surface
[INFO] [stdout]    ✔  Given w ← defaultWorld
[INFO] [stdout]    ✔  And shape ← the first object in w
[INFO] [stdout]    ✔  And r ← ray(point(0, 0, -5), vector(0, 0, 1))
[INFO] [stdout]    ✔  And xs ← intersections(4:shape, 6:shape)
[INFO] [stdout]    ✔  When comps ← prepare_computations(xs[0], r, xs)
[INFO] [stdout]    ✔  And c ← refractedColor(w, comps, 5)
[INFO] [stdout]    ✔  Then c = color(0, 0, 0)
[INFO] [stdout]   Scenario: The refracted color at the maximum recursive depth
[INFO] [stdout]    ✔  Given w ← defaultWorld
[INFO] [stdout]    ✔  And shape ← the first object in w
[INFO] [stdout]    ✔  And shape has:
[INFO] [stdout]        | material.transparency     | 1.0 |
[INFO] [stdout]        | material.refractive_index | 1.5 |
[INFO] [stdout]    ✔  And r ← ray(point(0, 0, -5), vector(0, 0, 1))
[INFO] [stdout]    ✔  And xs ← intersections(4:shape, 6:shape)
[INFO] [stdout]    ✔  When comps ← prepare_computations(xs[0], r, xs)
[INFO] [stdout]    ✔  And c ← refractedColor(w, comps, 0)
[INFO] [stdout]    ✔  Then c = color(0, 0, 0)
[INFO] [stdout]   Scenario: The refracted color under total internal reflection
[INFO] [stdout]    ✔  Given w ← defaultWorld
[INFO] [stdout]    ✔  And shape ← the first object in w
[INFO] [stdout]    ✔  And shape has:
[INFO] [stdout]        | material.transparency     | 1.0 |
[INFO] [stdout]        | material.refractive_index | 1.5 |
[INFO] [stdout]    ✔  And r ← ray(point(0, 0, 0.70710678118), vector(0, 1, 0))
[INFO] [stdout]    ✔  And xs ← intersections(-0.70710678118:shape, 0.70710678118:shape)
[INFO] [stdout]    ✔  When comps ← prepare_computations(xs[1], r, xs)
[INFO] [stdout]    ✔  And c ← refractedColor(w, comps, 5)
[INFO] [stdout]    ✔  Then c = color(0, 0, 0)
[INFO] [stdout]   Scenario: shade_hit() with a transparent material
[INFO] [stdout]    ✔  Given w ← defaultWorld
[INFO] [stdout]    ✔  And floor ← plane() with:
[INFO] [stdout]        | transform                 | translation(0, -1, 0) |
[INFO] [stdout]        | material.transparency     | 0.5                   |
[INFO] [stdout]        | material.refractive_index | 1.5                   |
[INFO] [stdout]    ✔  And floor is added to w
[INFO] [stdout]    ✔  And ball ← sphere with:
[INFO] [stdout]        | material.color   | (1, 0, 0)                  |
[INFO] [stdout]        | material.ambient | 0.5                        |
[INFO] [stdout]        | transform        | translation(0, -3.5, -0.5) |
[INFO] [stdout]    ✔  And ball is added to w
[INFO] [stdout]    ✔  And r ← ray(point(0, 0, -3), vector(0, -0.70710678118, 0.70710678118))
[INFO] [stdout]    ✔  And xs ← intersections(1.41421356237:floor)
[INFO] [stdout]    ✔  When comps ← prepare_computations(xs[0], r, xs)
[INFO] [stdout]    ✔  And color ← shade_hit(w, comps, 5)
[INFO] [stdout]    ✔  Then color = color(0.93642, 0.68642, 0.68642)
[INFO] [stdout]   Scenario: shade_hit() with a reflective, transparent material
[INFO] [stdout]    ✔  Given w ← defaultWorld
[INFO] [stdout]    ✔  And r ← ray(point(0, 0, -3), vector(0, -0.70710678118, 0.70710678118))
[INFO] [stdout]    ✔  And floor ← plane() with:
[INFO] [stdout]        | transform                 | translation(0, -1, 0) |
[INFO] [stdout]        | material.reflective       | 0.5                   |
[INFO] [stdout]        | material.transparency     | 0.5                   |
[INFO] [stdout]        | material.refractive_index | 1.5                   |
[INFO] [stdout]    ✔  And floor is added to w
[INFO] [stdout]    ✔  And ball ← sphere with:
[INFO] [stdout]        | material.color   | (1, 0, 0)                  |
[INFO] [stdout]        | material.ambient | 0.5                        |
[INFO] [stdout]        | transform        | translation(0, -3.5, -0.5) |
[INFO] [stdout]    ✔  And ball is added to w
[INFO] [stdout]    ✔  And xs ← intersections(1.41421356237:floor)
[INFO] [stdout]    ✔  When comps ← prepare_computations(xs[0], r, xs)
[INFO] [stdout]    ✔  And color ← shade_hit(w, comps, 5)
[INFO] [stdout]    ✔  Then color = color(0.93391, 0.69643, 0.69243)
[INFO] [stdout] [Summary]
[INFO] [stdout] 22 features
[INFO] [stdout] 313 scenarios (313 passed)
[INFO] [stdout] 1775 steps (1775 passed)
[INFO] [stderr]    Doc-tests ray_tracer_challenge
[INFO] [stdout] 
[INFO] [stdout] running 0 tests
[INFO] [stdout] 
[INFO] [stdout] test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
[INFO] [stdout] 
[INFO] running `Command { std: "docker" "inspect" "36ed3a99f224ec607efb46dca28b145504e2bfc7a74cef79729957520fdc650b", kill_on_drop: false }`
[INFO] running `Command { std: "docker" "rm" "-f" "36ed3a99f224ec607efb46dca28b145504e2bfc7a74cef79729957520fdc650b", kill_on_drop: false }`
[INFO] [stdout] 36ed3a99f224ec607efb46dca28b145504e2bfc7a74cef79729957520fdc650b
