Files
feather-case/test/config_test.rb
T
2026-08-04 12:27:31 -07:00

278 lines
9.3 KiB
Ruby

# frozen_string_literal: true
require_relative "test_helper"
class ConfigTest < Minitest::Test
def config
FeatherCaseWizard::Config.create(name: "Bench GPS", slug: "bench-gps")
end
def test_default_case_has_expected_parts_and_dimensions
assert_equal %w[body lid], config.printable_parts
assert_in_delta 61.6, config.dimensions["length"], 0.01
assert_in_delta 41.66, config.dimensions["width"], 0.01
assert_in_delta 12.25, config.parameters["wing_pitch"], 0.001
assert_in_delta 6.0, config.parameters["gps_antenna_height"], 0.001
assert_in_delta 2.4, config.parameters["oled_component_height"], 0.001
assert_in_delta 1.5, config.parameters["oled_display_height"], 0.001
assert_in_delta 2.0, config.parameters["usb_board_wall_gap"], 0.001
assert config.parameters["usb_opening_enabled"]
assert config.parameters["usb_guide_enabled"]
assert_equal ["3mf"], config.outputs["formats"]
end
def test_gps_height_uses_stack_pitch_antenna_height_and_top_gap
subject = config
subject.parameters["top_profile"] = "gps"
parameters = subject.parameters
expected_electronics_height =
parameters["wing_pitch"] + parameters["gps_antenna_height"] +
parameters["gps_top_gap"]
assert_in_delta 23.25, expected_electronics_height, 0.001
assert_in_delta 35.85, subject.dimensions["height"] + parameters["lid_thickness"], 0.001
end
def test_oled_height_uses_tallest_component_not_display_reference
subject = config
subject.parameters["top_profile"] = "oled128x64"
original_height = subject.dimensions["height"]
subject.parameters["oled_display_height"] = 10.0
assert_in_delta original_height, subject.dimensions["height"], 0.001
end
def test_older_config_receives_default_oled_display_height
data = FeatherCaseWizard::Config.deep_copy(config.data)
data["parameters"].delete("oled_display_height")
loaded = FeatherCaseWizard::Config.new(data)
assert_in_delta 1.5, loaded.parameters["oled_display_height"], 0.001
end
def test_older_config_preserves_opening_without_enabling_guide
data = FeatherCaseWizard::Config.deep_copy(config.data)
data["parameters"].delete("usb_opening_enabled")
data["parameters"].delete("usb_guide_enabled")
loaded = FeatherCaseWizard::Config.new(data)
assert loaded.parameters["usb_opening_enabled"]
refute loaded.parameters["usb_guide_enabled"]
end
def test_usb_guide_requires_opening
subject = config
subject.parameters["usb_opening_enabled"] = false
error = assert_raises(FeatherCaseWizard::ConfigError) { subject.validate! }
assert_match(/guide requires/, error.message)
end
def test_end_clearances_expand_case_length
subject = config
subject.parameters["usb_board_wall_gap"] = 20.0
subject.parameters["board_rear_clearance"] = 20.0
assert_in_delta 95.6, subject.dimensions["length"], 0.01
end
def test_insert_defaults_match_selected_hardware
parameters = config.parameters
assert_in_delta 4.0, parameters["m25_insert_length"], 0.001
assert_in_delta 3.1, parameters["m25_insert_hole_diameter"], 0.001
assert_in_delta 3.5, parameters["m25_insert_outer_diameter"], 0.001
assert_in_delta 5.0, parameters["tripod_insert_length"], 0.001
assert_in_delta 7.2, parameters["tripod_insert_hole_diameter"], 0.001
assert_in_delta 8.0, parameters["tripod_insert_outer_diameter"], 0.001
end
def test_tripod_floor_tracks_insert_length
subject = config
subject.parameters["tripod_insert_enabled"] = true
assert_in_delta 7.0, subject.dimensions["floor"], 0.001
end
def test_insert_coupon_uses_finished_pilot_ranges
source = FeatherCaseWizard::MODEL_SOURCE.read
assert_includes source,
"m25_coupon_steps = [-0.15, -0.075, 0, 0.075, 0.15];"
assert_includes source,
"tripod_coupon_steps = [-0.3, -0.15, 0, 0.15, 0.3];"
assert_includes source, "bore_d = diameter;"
refute_includes source, "bore_d = diameter + hole_compensation;"
end
def test_oled_buttons_and_selected_coupons_are_printable
subject = config
subject.parameters["top_profile"] = "oled128x64"
subject.parameters["button_a"] = false
subject.parameters["button_b"] = true
subject.outputs["coupons"] = %w[fit_coupon]
assert_equal %w[body lid buttons fit_coupon], subject.printable_parts
end
def test_combined_battery_and_tripod_case_adds_partition_clearance
subject = config
subject.parameters["battery_enabled"] = true
subject.parameters["tripod_insert_enabled"] = true
assert_in_delta 88.6, subject.dimensions["length"], 0.01
assert_in_delta 59.06, subject.dimensions["width"], 0.01
assert_in_delta 31.25, subject.dimensions["height"], 0.01
end
def test_legacy_round_switch_fields_migrate_to_rocker_defaults
data = config.data
data["parameters"]["switch_hole_diameter"] = 12.0
data["parameters"]["switch_body_length"] = 24.0
subject = FeatherCaseWizard::Config.new(data)
refute subject.parameters.key?("switch_hole_diameter")
refute subject.parameters.key?("switch_body_length")
assert_equal [12.9, 19.4], subject.parameters["rocker_cutout_size"]
assert_equal [15.0, 21.0, 21.0], subject.parameters["rocker_body_size"]
end
def test_battery_partition_requires_three_06_mm_lines
subject = config
subject.parameters["battery_partition_thickness"] = 1.19
error = assert_raises(FeatherCaseWizard::ConfigError) { subject.validate! }
assert_match(/battery_partition_thickness/, error.message)
end
def test_insert_entry_cannot_be_smaller_than_pilot
subject = config
subject.parameters["m25_insert_outer_diameter"] = 3.0
error = assert_raises(FeatherCaseWizard::ConfigError) { subject.validate! }
assert_match(/m25_insert_outer_diameter/, error.message)
end
def test_lid_artwork_adds_base_and_accent_parts
subject = config
subject.parameters["lid_label"] = "GPS"
subject.parameters["lid_logo_enabled"] = true
assert subject.lid_artwork?
assert_equal %w[body lid lid_base lid_accent], subject.printable_parts
end
def test_lid_artwork_rejects_oled_profile
subject = config
subject.parameters["top_profile"] = "oled128x64"
subject.parameters["lid_label"] = "OLED"
error = assert_raises(FeatherCaseWizard::ConfigError) { subject.validate! }
assert_match(/not supported with the OLED profile/, error.message)
end
def test_lid_artwork_orientation_is_named
subject = config
subject.parameters["lid_art_orientation"] = "diagonal"
error = assert_raises(FeatherCaseWizard::ConfigError) { subject.validate! }
assert_match(/orientation/, error.message)
end
def test_wall_local_cutouts_map_to_model_coordinates
subject = config
length = subject.dimensions["length"]
width = subject.dimensions["width"]
subject.data["cutouts"] = [
cutout("front", 12.0),
cutout("rear", 13.0),
cutout("left", 14.0),
cutout("right", 15.0)
]
result = subject.scad_cutouts
assert_in_delta width - 12.0, result[0][2], 0.001
assert_in_delta 13.0, result[1][2], 0.001
assert_in_delta 14.0, result[2][2], 0.001
assert_in_delta length - 15.0, result[3][2], 0.001
end
def test_cutout_cannot_intersect_floor
subject = config
subject.data["cutouts"] = [cutout("rear", 12.0).merge("z" => 2.0)]
error = assert_raises(FeatherCaseWizard::ConfigError) { subject.validate! }
assert_match(/floor/, error.message)
end
def test_cutouts_on_same_wall_require_minimum_web
subject = config
first = cutout("rear", 18.0).merge("name" => "first")
second = cutout("rear", 24.0).merge("name" => "second")
subject.data["cutouts"] = [first, second]
error = assert_raises(FeatherCaseWizard::ConfigError) { subject.validate! }
assert_match(/too close/, error.message)
end
def test_front_cutout_cannot_overlap_usb_opening
subject = config
subject.data["cutouts"] = [
cutout("front", subject.dimensions["width"] / 2).merge("z" => 11.0)
]
error = assert_raises(FeatherCaseWizard::ConfigError) { subject.validate! }
assert_match(/USB opening or guide/, error.message)
end
def test_front_cutout_can_clear_usb_reservation
subject = config
subject.data["cutouts"] = [cutout("front", 10.0).merge("z" => 11.0)]
assert_same subject, subject.validate!
end
def test_yaml_round_trip_preserves_resolved_cutouts
subject = config
subject.data["cutouts"] = [cutout("left", 20.0)]
Dir.mktmpdir do |directory|
path = File.join(directory, "case.yml")
File.write(path, subject.to_yaml)
loaded = FeatherCaseWizard::Config.load(path)
assert_equal subject.data["cutouts"], loaded.data["cutouts"]
assert_equal subject.scad_cutouts, loaded.data.dig("resolved", "extra_cutouts")
end
end
def test_unsafe_yaml_is_rejected
Dir.mktmpdir do |directory|
path = File.join(directory, "case.yml")
File.write(path, "--- !ruby/object:Object {}\n")
assert_raises(FeatherCaseWizard::ConfigError) do
FeatherCaseWizard::Config.load(path)
end
end
end
private
def cutout(face, horizontal)
{
"name" => "#{face}-cable",
"face" => face,
"shape" => "roundrect",
"horizontal" => horizontal,
"z" => 12.0,
"width" => 6.0,
"height" => 4.0,
"radius" => 1.0
}
end
end