Initial MoonModel implementation
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
class ColorTest < Minitest::Test
|
||||
def test_orders_by_luminance_and_is_deterministic
|
||||
palette = [{ "name" => "white", "hex" => "#FFFFFF" }, { "name" => "black", "hex" => "#000000" }]
|
||||
ordered = MoonModel::Color.ordered_palette(palette)
|
||||
assert_equal %w[black white], ordered.map { |c| c["name"] }
|
||||
assert_equal MoonModel::Color.choose(0.4, ordered, 10, 11), MoonModel::Color.choose(0.4, ordered, 10, 11)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,35 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
class ConfigTest < Minitest::Test
|
||||
def test_x2d_maxima_and_derived_defaults
|
||||
config = MoonModel::Config.new
|
||||
assert_in_delta 231.5, config.maximum_diameter("one_piece"), 0.001
|
||||
assert_in_delta 463.0, config.maximum_diameter("eight_piece"), 0.001
|
||||
assert_in_delta 1.6, config["shell"]["wall_mm"], 0.001
|
||||
assert_in_delta 0.8, config["shell"]["minimum_feature_mm"], 0.001
|
||||
assert_in_delta 10.0, config["stand"]["base_clearance_mm"], 0.001
|
||||
end
|
||||
|
||||
def test_scale_ratio_uses_lunar_datum
|
||||
config = MoonModel::Config.new("size" => { "input" => "scale", "value" => 10_000_000 })
|
||||
assert_in_delta 347.48, config.diameter_mm, 0.001
|
||||
assert_equal "datum", config["size"]["meaning"]
|
||||
assert config.segmented?
|
||||
end
|
||||
|
||||
def test_box_forces_segments
|
||||
assert MoonModel::Config.new("purpose" => "box", "size" => { "value" => 80 }).segmented?
|
||||
end
|
||||
|
||||
def test_rejects_bad_color
|
||||
assert_raises(ArgumentError) { MoonModel::Config.new("palette" => [{ "name" => "bad", "hex" => "gray" }]) }
|
||||
end
|
||||
|
||||
def test_rejects_negative_stand_clearance
|
||||
assert_raises(ArgumentError) do
|
||||
MoonModel::Config.new("stand" => { "base_clearance_mm" => -0.1 })
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,14 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
class DataSetTest < Minitest::Test
|
||||
def test_reads_and_wraps_data
|
||||
Dir.mktmpdir do |dir|
|
||||
data = MoonModel::DataSet.new(make_data(File.join(dir, "data.gz")))
|
||||
assert_equal 16, data.width
|
||||
assert_in_delta data.reflectance(0, 0), data.reflectance(0, 360), 1e-9
|
||||
assert_in_delta 300, data.relief_range_m, 1e-9
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,127 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
class GeneratorTest < Minitest::Test
|
||||
def test_one_piece_multicolor_package
|
||||
Dir.mktmpdir do |dir|
|
||||
data = MoonModel::DataSet.new(make_data(File.join(dir, "data.gz")))
|
||||
config = MoonModel::Config.new(
|
||||
"size" => { "value" => 40 }, "resolution" => { "longitude_segments" => 24 },
|
||||
"palette" => [{ "name" => "dark", "hex" => "#222222" }, { "name" => "light", "hex" => "#DDDDDD" }],
|
||||
"grid" => { "enabled" => true, "color_name" => "light" }, "output" => { "name" => "test" }
|
||||
)
|
||||
result = MoonModel::Generator.new(config, data_set: data, output_dir: File.join(dir, "out")).generate
|
||||
path = File.join(result[:output_dir], "moon.3mf")
|
||||
assert File.file?(path)
|
||||
Zip::File.open(path) do |zip|
|
||||
model = zip.read("3D/3dmodel.model")
|
||||
assert_includes model, "<basematerials"
|
||||
assert_includes model, "displaycolor=\"#222222\""
|
||||
assert_includes model, "<components>"
|
||||
end
|
||||
assert File.file?(File.join(result[:output_dir], "manifest.md"))
|
||||
assert_in_delta 40.0, result[:statistics]["final_envelope_mm"], 0.01
|
||||
end
|
||||
end
|
||||
|
||||
def test_segmented_box_outputs_parts_keys_and_stand
|
||||
Dir.mktmpdir do |dir|
|
||||
data = MoonModel::DataSet.new(make_data(File.join(dir, "data.gz")))
|
||||
config = MoonModel::Config.new("purpose" => "box", "size" => { "value" => 50 },
|
||||
"stand" => { "enabled" => true },
|
||||
"resolution" => { "longitude_segments" => 24 })
|
||||
result = MoonModel::Generator.new(config, data_set: data, output_dir: File.join(dir, "out")).generate
|
||||
assert_equal 8, Dir[File.join(result[:output_dir], "north_*.3mf")].length + Dir[File.join(result[:output_dir], "south_*.3mf")].length
|
||||
assert File.file?(File.join(result[:output_dir], "assembly_keys.3mf"))
|
||||
assert File.file?(File.join(result[:output_dir], "orbital_triskelion_stand.3mf"))
|
||||
manifest = File.read(File.join(result[:output_dir], "manifest.md"))
|
||||
assert_includes manifest, "Stand style: Orbital triskelion"
|
||||
assert_includes manifest, "Terrain envelope v2 (rotation independent)"
|
||||
assert_includes manifest, "Guaranteed Moon clearance above hub/base: 10.00 mm"
|
||||
assert_includes manifest, "Stand dimensions:"
|
||||
end
|
||||
end
|
||||
|
||||
def test_invalid_preflight_does_not_create_output_directory
|
||||
Dir.mktmpdir do |dir|
|
||||
data = MoonModel::DataSet.new(make_data(File.join(dir, "data.gz")))
|
||||
config = MoonModel::Config.new("size" => { "value" => 40, "meaning" => "datum" },
|
||||
"vertical_exaggeration" => 100_000,
|
||||
"resolution" => { "longitude_segments" => 24 })
|
||||
output = File.join(dir, "out")
|
||||
|
||||
assert_raises(ArgumentError) { MoonModel::Generator.new(config, data_set: data, output_dir: output).generate }
|
||||
refute Dir.exist?(output)
|
||||
end
|
||||
end
|
||||
|
||||
def test_revision_atomically_replaces_obsolete_files
|
||||
Dir.mktmpdir do |dir|
|
||||
data = MoonModel::DataSet.new(make_data(File.join(dir, "data.gz")))
|
||||
output = File.join(dir, "named_moon")
|
||||
common = { "size" => { "value" => 40 }, "resolution" => { "longitude_segments" => 24 },
|
||||
"output" => { "name" => "Named Moon" } }
|
||||
MoonModel::Generator.new(MoonModel::Config.new(common), data_set: data, output_dir: output).generate
|
||||
assert File.file?(File.join(output, "moon.3mf"))
|
||||
|
||||
revised = MoonModel::Config.new(common.merge("assembly" => "eight_piece"))
|
||||
MoonModel::Generator.new(revised, data_set: data, output_dir: output).generate
|
||||
|
||||
refute File.exist?(File.join(output, "moon.3mf"))
|
||||
assert File.file?(File.join(output, "north_1.3mf"))
|
||||
assert MoonModel::Generator.generated_directory?(output)
|
||||
end
|
||||
end
|
||||
|
||||
def test_refuses_to_replace_unrelated_directory
|
||||
Dir.mktmpdir do |dir|
|
||||
output = File.join(dir, "not_a_model")
|
||||
Dir.mkdir(output)
|
||||
File.write(File.join(output, "keep.txt"), "important")
|
||||
data = MoonModel::DataSet.new(make_data(File.join(dir, "data.gz")))
|
||||
config = MoonModel::Config.new("size" => { "value" => 40 },
|
||||
"resolution" => { "longitude_segments" => 24 })
|
||||
|
||||
assert_raises(ArgumentError) { MoonModel::Generator.new(config, data_set: data, output_dir: output).generate }
|
||||
assert_equal "important", File.read(File.join(output, "keep.txt"))
|
||||
end
|
||||
end
|
||||
|
||||
def test_failed_atomic_install_restores_previous_model
|
||||
Dir.mktmpdir do |dir|
|
||||
data = MoonModel::DataSet.new(make_data(File.join(dir, "data.gz")))
|
||||
output = File.join(dir, "recoverable")
|
||||
common = { "size" => { "value" => 40 }, "resolution" => { "longitude_segments" => 24 },
|
||||
"output" => { "name" => "Recoverable" } }
|
||||
MoonModel::Generator.new(MoonModel::Config.new(common), data_set: data, output_dir: output).generate
|
||||
original = File.binread(File.join(output, "moon.3mf"))
|
||||
failing_class = Class.new(MoonModel::Generator) do
|
||||
private
|
||||
|
||||
def rename_path(from, to)
|
||||
@rename_count = @rename_count.to_i + 1
|
||||
raise IOError, "simulated install failure" if @rename_count == 2
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
error = assert_raises(IOError) do
|
||||
failing_class.new(MoonModel::Config.new(common.merge("assembly" => "eight_piece")),
|
||||
data_set: data, output_dir: output).generate
|
||||
end
|
||||
assert_equal "simulated install failure", error.message
|
||||
assert_equal original, File.binread(File.join(output, "moon.3mf"))
|
||||
assert_empty Dir[File.join(dir, ".recoverable.*")]
|
||||
end
|
||||
end
|
||||
|
||||
def test_safe_model_name_and_default_build_directory
|
||||
config = MoonModel::Config.new("output" => { "name" => "My 8-inch Moon!" })
|
||||
generator = MoonModel::Generator.new(config)
|
||||
|
||||
assert_equal "My_8-inch_Moon", MoonModel::Generator.safe_name("My 8-inch Moon!")
|
||||
assert_equal File.expand_path("../build/My_8-inch_Moon", __dir__), generator.output_dir
|
||||
assert_raises(ArgumentError) { MoonModel::Generator.safe_name("...") }
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,66 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
class SizingTest < Minitest::Test
|
||||
def setup
|
||||
@data = MoonModel::DataSet.new
|
||||
end
|
||||
|
||||
def datum_config(exaggeration, assembly: "auto")
|
||||
MoonModel::Config.new(
|
||||
"size" => { "input" => "diameter", "value" => 8.0, "unit" => "in", "meaning" => "datum" },
|
||||
"vertical_exaggeration" => exaggeration,
|
||||
"assembly" => assembly,
|
||||
"resolution" => { "longitude_segments" => 48 }
|
||||
)
|
||||
end
|
||||
|
||||
def test_eight_inch_relief_uses_consistent_units
|
||||
one = MoonModel::Sizing.calculate(datum_config(1), @data)
|
||||
ten = MoonModel::Sizing.calculate(datum_config(10), @data)
|
||||
|
||||
assert_in_delta 1.153, one.relief_range_mm, 0.001
|
||||
assert_in_delta 11.532, ten.relief_range_mm, 0.001
|
||||
assert_operator ten.final_envelope_mm, :>, one.final_envelope_mm
|
||||
assert_operator ten.maximum_radius_mm, :>, ten.minimum_radius_mm
|
||||
assert_operator ten.maximum_radius_mm, :>, ten.datum_diameter_mm / 2.0
|
||||
end
|
||||
|
||||
|
||||
def test_single_color_grid_is_included_in_conservative_minimum_radius
|
||||
plain = datum_config(5)
|
||||
grid = datum_config(5)
|
||||
grid = MoonModel::Config.new(grid.to_h.merge("grid" => { "enabled" => true, "depth_mm" => 0.7 }))
|
||||
|
||||
plain_result = MoonModel::Sizing.calculate(plain, @data)
|
||||
grid_result = MoonModel::Sizing.calculate(grid, @data)
|
||||
|
||||
assert_in_delta 0.7, plain_result.minimum_radius_mm - grid_result.minimum_radius_mm, 0.001
|
||||
end
|
||||
|
||||
def test_unsafe_exaggeration_is_rejected_with_limit
|
||||
error = assert_raises(ArgumentError) { MoonModel::Sizing.calculate(datum_config(1000), @data) }
|
||||
|
||||
assert_match(/maximum is 18[89]x/, error.message)
|
||||
end
|
||||
|
||||
def test_exaggerated_datum_can_automatically_select_sections
|
||||
result = MoonModel::Sizing.calculate(datum_config(100), @data)
|
||||
|
||||
assert result.segmented?
|
||||
assert_equal "automatic", result.mode_source
|
||||
end
|
||||
|
||||
def test_final_envelope_remains_requested_size
|
||||
config = MoonModel::Config.new(
|
||||
"size" => { "input" => "diameter", "value" => 8.0, "unit" => "in", "meaning" => "final_envelope" },
|
||||
"vertical_exaggeration" => 10,
|
||||
"resolution" => { "longitude_segments" => 48 }
|
||||
)
|
||||
result = MoonModel::Sizing.calculate(config, @data)
|
||||
|
||||
assert_in_delta 203.2, result.final_envelope_mm, 0.001
|
||||
refute result.segmented?
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,87 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
class StandTest < Minitest::Test
|
||||
def setup
|
||||
@config = MoonModel::Config.new("stand" => { "enabled" => true })
|
||||
end
|
||||
|
||||
def test_orbital_stand_is_printable_and_scales_for_common_moons
|
||||
[30.0, 120.0, 203.2].each do |diameter|
|
||||
result = MoonModel::Stand.build(@config, diameter)
|
||||
stats = result.statistics
|
||||
|
||||
assert result.mesh.valid?
|
||||
assert_manifold result.mesh
|
||||
assert_equal "orbital_triskelion_stand", result.mesh.name
|
||||
assert_equal "orbital_triskelion", stats["style"]
|
||||
assert_in_delta 0.0, result.mesh.bounds[2][0], 1e-9
|
||||
assert_operator stats["maximum_underside_slope"], :<=, 1.0
|
||||
assert_operator stats["contact_radius_mm"], :<, diameter / 2.0
|
||||
assert_operator stats["contact_height_mm"], :>, stats["moon_bottom_clearance_mm"]
|
||||
assert_in_delta 10.0, stats["guaranteed_hub_clearance_mm"], 1e-9
|
||||
assert_in_delta stats["hub_height_mm"] + 10.0, stats["guaranteed_moon_bottom_height_mm"], 1e-9
|
||||
refute stats["bed_capped"]
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def test_eight_inch_exaggerated_moon_clears_hub_in_any_rotation
|
||||
config = MoonModel::Config.new(
|
||||
"size" => { "input" => "diameter", "value" => 8.0, "unit" => "in", "meaning" => "datum" },
|
||||
"vertical_exaggeration" => 5.0,
|
||||
"stand" => { "enabled" => true },
|
||||
"resolution" => { "longitude_segments" => 96 }
|
||||
)
|
||||
sizing = MoonModel::Sizing.calculate(config, MoonModel::DataSet.new)
|
||||
result = MoonModel::Stand.build(config, "envelope_mm" => sizing.final_envelope_mm,
|
||||
"minimum_radius_mm" => sizing.minimum_radius_mm,
|
||||
"maximum_radius_mm" => sizing.maximum_radius_mm)
|
||||
stats = result.statistics
|
||||
|
||||
assert_equal "terrain_envelope_v2", stats["safety_model"]
|
||||
assert_in_delta 10.0, stats["guaranteed_hub_clearance_mm"], 1e-9
|
||||
assert_operator stats["guaranteed_moon_bottom_height_mm"], :>=, stats["hub_height_mm"] + 10.0
|
||||
assert_operator stats["contact_height_mm"], :>, 30.0
|
||||
assert_operator stats["maximum_underside_slope"], :<=, 1.0
|
||||
end
|
||||
|
||||
def test_custom_clearance_changes_guaranteed_gap
|
||||
config = MoonModel::Config.new("stand" => { "enabled" => true, "base_clearance_mm" => 4.0 })
|
||||
result = MoonModel::Stand.build(config, 120.0)
|
||||
|
||||
assert_in_delta 4.0, result.statistics["guaranteed_hub_clearance_mm"], 1e-9
|
||||
end
|
||||
|
||||
def test_large_moon_stand_is_capped_to_usable_bed
|
||||
result = MoonModel::Stand.build(@config, 463.0)
|
||||
usable = @config.build_volume.first(2).map { |value| value - 2.0 * @config["edge_clearance_mm"] }
|
||||
|
||||
assert result.statistics["bed_capped"]
|
||||
assert_operator result.statistics["dimensions_mm"][0], :<=, usable[0]
|
||||
assert_operator result.statistics["dimensions_mm"][1], :<=, usable[1]
|
||||
assert_operator result.statistics["dimensions_mm"][2], :<=, @config.build_volume[2]
|
||||
end
|
||||
|
||||
def test_legacy_stand_configuration_gets_new_style
|
||||
config = MoonModel::Config.new("stand" => { "enabled" => true, "material" => nil })
|
||||
|
||||
assert_equal "orbital_triskelion", config["stand"]["style"]
|
||||
assert_equal 10.0, config["stand"]["base_clearance_mm"]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def assert_manifold(mesh)
|
||||
edge_uses = Hash.new(0)
|
||||
mesh.triangles.each do |triangle|
|
||||
triangle.each_index do |index|
|
||||
edge = [triangle[index], triangle[(index + 1) % triangle.length]].sort
|
||||
edge_uses[edge] += 1
|
||||
end
|
||||
end
|
||||
|
||||
assert edge_uses.values.all? { |count| count == 2 }, "every mesh edge must belong to exactly two faces"
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,28 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "minitest/autorun"
|
||||
require "tmpdir"
|
||||
require "zip"
|
||||
require "moon_model"
|
||||
|
||||
module TestData
|
||||
def make_data(path, width: 16, height: 8)
|
||||
elevations = Array.new(width * height) { |i| ((i % width) - width / 2) * 20 }
|
||||
reflectance = Array.new(width * height) { |i| (255.0 * (i % width) / (width - 1)).round }
|
||||
header = { "format_version" => 1, "title" => "test data", "width" => width, "height" => height,
|
||||
"elevation_offset_range_m" => elevations.minmax }
|
||||
encoded = JSON.generate(header)
|
||||
Zlib::GzipWriter.open(path) do |gzip|
|
||||
gzip.write(MoonModel::DataSet::MAGIC)
|
||||
gzip.write([encoded.bytesize].pack("L<"))
|
||||
gzip.write(encoded)
|
||||
gzip.write(elevations.pack("s<*"))
|
||||
gzip.write(reflectance.pack("C*"))
|
||||
end
|
||||
path
|
||||
end
|
||||
end
|
||||
|
||||
class Minitest::Test
|
||||
include TestData
|
||||
end
|
||||
@@ -0,0 +1,65 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
class ThreeMFTest < Minitest::Test
|
||||
def test_bambu_part_names_and_slots_follow_palette_order
|
||||
palette = [
|
||||
{ "name" => "Bright & Structural", "hex" => "#EEEEEE" },
|
||||
{ "name" => "Dark", "hex" => "#222222" },
|
||||
{ "name" => "Mid gray", "hex" => "#888888" },
|
||||
{ "name" => "Blue gray", "hex" => "#667788" }
|
||||
]
|
||||
meshes = [mesh("moon_structure", palette[0], 0)] +
|
||||
palette.each_with_index.map { |color, index| mesh("moon_surface_#{index + 1}", color, index + 2) }
|
||||
|
||||
Dir.mktmpdir do |dir|
|
||||
path = File.join(dir, "moon.3mf")
|
||||
MoonModel::ThreeMF.write(path, meshes, metadata: { "Title" => "Four Color Moon" }, palette: palette)
|
||||
Zip::File.open(path) do |zip|
|
||||
model = zip.read("3D/3dmodel.model").force_encoding(Encoding::UTF_8)
|
||||
settings = zip.read("Metadata/model_settings.config").force_encoding(Encoding::UTF_8)
|
||||
|
||||
assert_operator model.index('name="Bright & Structural"'), :<, model.index('name="Dark"')
|
||||
assert_includes model, 'name="Four Color Moon" type="model"><components>'
|
||||
assert_includes settings, 'value="Structure — Bright & Structural (#EEEEEE) — Filament 1"'
|
||||
assert_includes settings, 'value="Surface — Bright & Structural (#EEEEEE) — Filament 1"'
|
||||
assert_equal 2, settings.scan('key="extruder" value="1"').length
|
||||
assert_equal 1, settings.scan('key="extruder" value="2"').length
|
||||
assert_equal 1, settings.scan('key="extruder" value="3"').length
|
||||
assert_equal 1, settings.scan('key="extruder" value="4"').length
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_unassigned_mesh_has_name_but_no_extruder
|
||||
mesh = mesh("three_leg_stand", nil, 0)
|
||||
|
||||
settings = MoonModel::ThreeMF.send(:build_bambu_model_settings, [mesh], { "Title" => "Stand" }, [])
|
||||
|
||||
assert_includes settings, "Three Leg Stand — material unassigned"
|
||||
refute_includes settings, 'key="extruder"'
|
||||
end
|
||||
|
||||
def test_unused_colors_do_not_renumber_later_palette_slots
|
||||
palette = [
|
||||
{ "name" => "First", "hex" => "#111111" },
|
||||
{ "name" => "Unused", "hex" => "#222222" },
|
||||
{ "name" => "Third", "hex" => "#333333" }
|
||||
]
|
||||
meshes = [mesh("moon_structure", palette[0], 0), mesh("moon_third", palette[2], 2)]
|
||||
|
||||
settings = MoonModel::ThreeMF.send(:build_bambu_model_settings, meshes, { "Title" => "Moon" }, palette)
|
||||
|
||||
assert_includes settings, "Surface — Third (#333333) — Filament 3"
|
||||
assert_includes settings, 'key="extruder" value="3"'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def mesh(name, material, offset)
|
||||
MoonModel::Mesh.new(name: name, material: material).tap do |value|
|
||||
value.add_box([offset, 0, 0], [1, 1, 1])
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,80 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
require "stringio"
|
||||
|
||||
class WizardTest < Minitest::Test
|
||||
def test_blank_input_accepts_false_default
|
||||
answer, output = ask_yes_no("\n", false)
|
||||
|
||||
refute answer
|
||||
assert_includes output, "|no|"
|
||||
refute_includes output, 'Please enter "yes" or "no".'
|
||||
end
|
||||
|
||||
def test_blank_input_accepts_true_default
|
||||
answer, output = ask_yes_no("\n", true)
|
||||
|
||||
assert answer
|
||||
assert_includes output, "|yes|"
|
||||
refute_includes output, 'Please enter "yes" or "no".'
|
||||
end
|
||||
|
||||
def test_explicit_answers_override_defaults
|
||||
{ "yes\n" => true, "y\n" => true, "no\n" => false, "n\n" => false }.each do |input, expected|
|
||||
answer, = ask_yes_no(input, !expected)
|
||||
assert_equal expected, answer, "expected #{input.inspect} to override the default"
|
||||
end
|
||||
end
|
||||
|
||||
def test_selects_a_new_named_model
|
||||
Dir.mktmpdir do |dir|
|
||||
output = StringIO.new
|
||||
wizard = MoonModel::Wizard.new(io: HighLine.new(StringIO.new("My Moon\n"), output), output_root: dir)
|
||||
|
||||
values = wizard.send(:select_model)
|
||||
|
||||
assert_equal "My Moon", values.dig("output", "name")
|
||||
refute Dir.exist?(File.join(dir, "My_Moon"))
|
||||
end
|
||||
end
|
||||
|
||||
def test_existing_model_can_seed_revision_defaults
|
||||
Dir.mktmpdir do |dir|
|
||||
model = File.join(dir, "Saved_Moon")
|
||||
Dir.mkdir(model)
|
||||
config = MoonModel::Config.new("vertical_exaggeration" => 5, "output" => { "name" => "Saved Moon" })
|
||||
config.save(File.join(model, "config.yml"))
|
||||
File.write(File.join(model, "manifest.md"), "Generated by MoonModel test")
|
||||
output = StringIO.new
|
||||
wizard = MoonModel::Wizard.new(io: HighLine.new(StringIO.new("Saved Moon\n\n"), output), output_root: dir)
|
||||
|
||||
values = wizard.send(:select_model)
|
||||
|
||||
assert_equal 5, values["vertical_exaggeration"]
|
||||
assert_equal "Saved Moon", values.dig("output", "name")
|
||||
assert_includes output.string, "Revise the existing Saved Moon model?"
|
||||
end
|
||||
end
|
||||
|
||||
def test_warns_when_user_chooses_less_than_recommended_stand_clearance
|
||||
output = StringIO.new
|
||||
highline = HighLine.new(StringIO.new("\n\n5\n"), output)
|
||||
wizard = MoonModel::Wizard.new(io: highline)
|
||||
values = { "purpose" => "display", "assembly" => "auto" }
|
||||
|
||||
wizard.send(:purpose, values)
|
||||
|
||||
assert_equal 5.0, values.dig("stand", "base_clearance_mm")
|
||||
assert_includes output.string, "waive the recommended terrain safety margin"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ask_yes_no(input, default)
|
||||
output = StringIO.new
|
||||
highline = HighLine.new(StringIO.new(input), output)
|
||||
wizard = MoonModel::Wizard.new(io: highline)
|
||||
[wizard.send(:yes_no, "Continue?", default), output.string]
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user