1
0
Fork 0
forked from OpenNeo/impress

Add image comparison tests for OutfitImageRenderer

This commit is contained in:
Emi Matchu 2026-01-04 13:31:01 -08:00
parent 55fa50c22a
commit aa45ea17b3
6 changed files with 102 additions and 52 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

BIN
spec/fixtures/outfit_images/Hat.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -28,9 +28,9 @@ hindbiology:
type_id: 1
label: Hind Biology
plain_label: hindbiology
markings:
id: 31
depth: 35
markings1:
id: 6
depth: 8
type_id: 2
label: Markings
plain_label: markings
@ -88,6 +88,12 @@ body:
type_id: 1
label: Body
plain_label: body
markings2:
id: 16
depth: 19
type_id: 2
label: Markings
plain_label: markings
bodydisease:
id: 17
depth: 20
@ -172,6 +178,12 @@ head:
type_id: 1
label: Head
plain_label: head
markings3:
id: 31
depth: 35
type_id: 2
label: Markings
plain_label: markings
headdisease:
id: 32
depth: 36
@ -196,9 +208,9 @@ glasses:
type_id: 2
label: Glasses
plain_label: glasses
earrings:
id: 41
depth: 45
earrings1:
id: 36
depth: 39
type_id: 2
label: Earrings
plain_label: earrings
@ -220,15 +232,21 @@ headdrippings:
type_id: 1
label: Head Drippings
plain_label: headdrippings
hat:
id: 50
depth: 16
hat1:
id: 40
depth: 44
type_id: 2
label: Hat
plain_label: hat
righthanditem:
id: 49
depth: 5
earrings2:
id: 41
depth: 45
type_id: 2
label: Earrings
plain_label: earrings
righthanditem1:
id: 42
depth: 46
type_id: 2
label: Right-hand Item
plain_label: righthanditem
@ -268,6 +286,18 @@ backgrounditem:
type_id: 3
label: Background Item
plain_label: backgrounditem
righthanditem2:
id: 49
depth: 5
type_id: 2
label: Right-hand Item
plain_label: righthanditem
hat2:
id: 50
depth: 16
type_id: 2
label: Hat
plain_label: hat
belt:
id: 51
depth: 27

View file

@ -4,12 +4,10 @@ require_relative '../rails_helper'
RSpec.describe OutfitImageRenderer do
fixtures :zones, :colors, :species
# Helper to create a simple PNG image (1x1 pixel) with a specific color
def create_test_png(red, green, blue, alpha = 255)
require 'vips'
image = Vips::Image.black(1, 1, bands: 4)
image = image.new_from_image([red, green, blue, alpha])
image.write_to_buffer('.png')
# Helper to load a fixture image
def load_fixture_image(filename)
path = Rails.root.join('spec', 'fixtures', 'outfit_images', filename)
File.read(path)
end
# Helper to create a pet state with specific swf_assets
@ -84,25 +82,28 @@ RSpec.describe OutfitImageRenderer do
describe "#render" do
context "with a simple outfit" do
it "composites biology and item layers into a single PNG" do
# Create test PNG data
red_png = create_test_png(255, 0, 0) # Red pixel
blue_png = create_test_png(0, 0, 255) # Blue pixel
# Load fixture images
acara_png = load_fixture_image('Blue Acara.png')
hat_png = load_fixture_image('Hat.png')
expected_composite_png = load_fixture_image('Blue Acara With Hat.png')
# Create biology and item assets
biology_asset = build_biology_asset(zones(:head), body_id: 1)
item_asset = build_item_asset(zones(:hat), body_id: 1)
item_asset = build_item_asset(zones(:hat1), body_id: 1)
# Stub HTTP requests for the actual image URLs that will be generated
stub_request(:get, biology_asset.image_url).
to_return(body: red_png, status: 200)
to_return(body: acara_png, status: 200)
stub_request(:get, item_asset.image_url).
to_return(body: blue_png, status: 200)
to_return(body: hat_png, status: 200)
# Build outfit
pet_state = build_pet_state(@pet_type, swf_assets: [biology_asset])
item = build_item("Test Hat", swf_assets: [item_asset])
outfit = Outfit.new(pet_state: pet_state)
outfit.item_ids = { worn: [item.id], closeted: [] }
outfit = Outfit.new(
pet_state: pet_state,
worn_items: [item]
)
# Render
renderer = OutfitImageRenderer.new(outfit)
@ -114,29 +115,48 @@ RSpec.describe OutfitImageRenderer do
expect(result[0..7]).to eq("\x89PNG\r\n\x1A\n".b) # PNG magic bytes
# Verify the result is a valid 600x600 PNG
image = Vips::Image.new_from_buffer(result, "")
expect(image.width).to eq(600)
expect(image.height).to eq(600)
result_image = Vips::Image.new_from_buffer(result, "")
expect(result_image.width).to eq(600)
expect(result_image.height).to eq(600)
# Verify the composite matches the expected image pixel-perfectly
expected_image = Vips::Image.new_from_buffer(expected_composite_png, "")
# Calculate the absolute difference between images
diff = (result_image - expected_image).abs
max_diff = diff.max
# Allow a small tolerance for minor encoding/compositing differences
# The expected image was generated with a different method, so we expect
# very close but not necessarily pixel-perfect matches
tolerance = 2
if max_diff > tolerance
debug_path = Rails.root.join('tmp', 'test_render_result.png')
result_image.write_to_file(debug_path.to_s)
fail "Images should match within tolerance of #{tolerance}, but found max difference of #{max_diff}. Actual output saved to #{debug_path}"
end
end
end
context "when a layer image fails to load" do
it "skips the failed layer and continues" do
blue_png = create_test_png(0, 0, 255)
hat_png = load_fixture_image('Hat.png')
biology_asset = build_biology_asset(zones(:head), body_id: 1)
item_asset = build_item_asset(zones(:hat), body_id: 1)
item_asset = build_item_asset(zones(:hat1), body_id: 1)
# Stub one successful request and one failure
stub_request(:get, biology_asset.image_url).
to_return(status: 404)
stub_request(:get, item_asset.image_url).
to_return(body: blue_png, status: 200)
to_return(body: hat_png, status: 200)
pet_state = build_pet_state(@pet_type, swf_assets: [biology_asset])
item = build_item("Test Hat", swf_assets: [item_asset])
outfit = Outfit.new(pet_state: pet_state)
outfit.item_ids = { worn: [item.id], closeted: [] }
outfit = Outfit.new(
pet_state: pet_state,
worn_items: [item]
)
renderer = OutfitImageRenderer.new(outfit)
result = renderer.render

View file

@ -95,7 +95,7 @@ RSpec.describe Outfit do
pet_state = build_pet_state(@pet_type, swf_assets: [head, body])
# Create item layers
hat_asset = build_item_asset(zones(:hat), body_id: 1)
hat_asset = build_item_asset(zones(:hat1), body_id: 1)
hat = build_item("Test Hat", swf_assets: [hat_asset])
outfit = Outfit.new(pet_state: pet_state)
@ -134,7 +134,7 @@ RSpec.describe Outfit do
# Create a hat that restricts the hair zone
# zones_restrict is a bitfield where position 37 (Hair Front zone id) is "1"
zones_restrict = "0" * 36 + "1" + "0" * 20 # bit 37 = 1
hat_asset = build_item_asset(zones(:hat), body_id: 1, zones_restrict: zones_restrict)
hat_asset = build_item_asset(zones(:hat1), body_id: 1, zones_restrict: zones_restrict)
hat = build_item("Hair-hiding Hat", swf_assets: [hat_asset])
outfit = Outfit.new(pet_state: pet_state)
@ -156,7 +156,7 @@ RSpec.describe Outfit do
# Create an item that restricts both Hair Front (37) and Head Transient Biology (38)
zones_restrict = "0" * 36 + "11" + "0" * 20 # bits 37 and 38 = 1
hood_asset = build_item_asset(zones(:hat), body_id: 1, zones_restrict: zones_restrict)
hood_asset = build_item_asset(zones(:hat1), body_id: 1, zones_restrict: zones_restrict)
hood = build_item("Agent Hood", swf_assets: [hood_asset])
outfit = Outfit.new(pet_state: pet_state)
@ -222,7 +222,7 @@ RSpec.describe Outfit do
pet_state = build_pet_state(@pet_type, pose: "UNCONVERTED", swf_assets: [head, body])
# Create both body-specific and body_id=0 items
body_specific_asset = build_item_asset(zones(:hat), body_id: 1)
body_specific_asset = build_item_asset(zones(:hat1), body_id: 1)
body_specific_item = build_item("Body-specific Hat", swf_assets: [body_specific_asset])
universal_asset = build_item_asset(zones(:background), body_id: 0)
@ -244,7 +244,7 @@ RSpec.describe Outfit do
pet_state = build_pet_state(@pet_type, pose: "UNCONVERTED", swf_assets: [head])
# Create a body-specific item in a zone the pet doesn't restrict
hat_asset = build_item_asset(zones(:hat), body_id: 1)
hat_asset = build_item_asset(zones(:hat1), body_id: 1)
hat = build_item("Body-specific Hat", swf_assets: [hat_asset])
outfit = Outfit.new(pet_state: pet_state)
@ -296,7 +296,7 @@ RSpec.describe Outfit do
# Add an item that restricts Hair Front (37)
item_zones_restrict = "0" * 36 + "1" + "0" * 20 # bit 37 = 1
hat_asset = build_item_asset(zones(:hat), body_id: 1, zones_restrict: item_zones_restrict)
hat_asset = build_item_asset(zones(:hat1), body_id: 1, zones_restrict: item_zones_restrict)
hat = build_item("Hair-hiding Hat", swf_assets: [hat_asset])
outfit = Outfit.new(pet_state: pet_state)
@ -357,7 +357,7 @@ RSpec.describe Outfit do
# Add items at various depths
bg_item = build_item_asset(zones(:backgrounditem), body_id: 0) # depth 4
hat_asset = build_item_asset(zones(:hat), body_id: 1) # depth 16
hat_asset = build_item_asset(zones(:hat1), body_id: 1) # depth 44
shirt_asset = build_item_asset(zones(:shirtdress), body_id: 1) # depth 26
bg = build_item("Background Item", swf_assets: [bg_item])
@ -370,10 +370,10 @@ RSpec.describe Outfit do
layers = outfit.visible_layers
# Expected order by depth:
# background (3), bg_item (4), hat_asset (16), body_layer (18),
# shirt_asset (26), head_layer (34)
expect(layers.map(&:depth)).to eq([3, 4, 16, 18, 26, 34])
expect(layers).to eq([background, bg_item, hat_asset, body_layer, shirt_asset, head_layer])
# background (3), bg_item (4), body_layer (18), shirt_asset (26),
# head_layer (34), hat_asset (44)
expect(layers.map(&:depth)).to eq([3, 4, 18, 26, 34, 44])
expect(layers).to eq([background, bg_item, body_layer, shirt_asset, head_layer, hat_asset])
end
end
@ -417,7 +417,7 @@ RSpec.describe Outfit do
pet_state = build_pet_state(@pet_type)
# Create a body-specific item for the alt style's body_id
body_specific_asset = build_item_asset(zones(:hat), body_id: 999)
body_specific_asset = build_item_asset(zones(:hat1), body_id: 999)
body_specific_item = build_item("Body-specific Hat", swf_assets: [body_specific_asset])
# Create a universal item (body_id=0)
@ -441,7 +441,7 @@ RSpec.describe Outfit do
pet_state = build_pet_state(@pet_type)
# Create an item that fits the regular pet's body_id (1)
regular_item_asset = build_item_asset(zones(:hat), body_id: 1)
regular_item_asset = build_item_asset(zones(:hat1), body_id: 1)
regular_item = build_item("Regular Pet Hat", swf_assets: [regular_item_asset])
outfit = Outfit.new(pet_state: pet_state, alt_style: @alt_style)
@ -463,7 +463,7 @@ RSpec.describe Outfit do
# Create a universal hat that restricts the hair zone
zones_restrict = "0" * 36 + "1" + "0" * 20 # bit 37 (Hair Front) = 1
hat_asset = build_item_asset(zones(:hat), body_id: 0, zones_restrict: zones_restrict)
hat_asset = build_item_asset(zones(:hat1), body_id: 0, zones_restrict: zones_restrict)
hat = build_item("Hair-hiding Hat", swf_assets: [hat_asset])
outfit = Outfit.new(pet_state: pet_state, alt_style: @alt_style)
@ -526,7 +526,7 @@ RSpec.describe Outfit do
# Add universal items at various depths
bg_item = build_item_asset(zones(:backgrounditem), body_id: 0) # depth 4
trinket = build_item_asset(zones(:righthanditem), body_id: 0) # depth 5
trinket = build_item_asset(zones(:righthanditem1), body_id: 0) # depth 46
bg = build_item("Background Item", swf_assets: [bg_item])
trinket_item = build_item("Trinket", swf_assets: [trinket])
@ -537,9 +537,9 @@ RSpec.describe Outfit do
layers = outfit.visible_layers
# Expected order by depth:
# alt_background (3), bg_item (4), trinket (5), alt_body (18), alt_head (34)
expect(layers.map(&:depth)).to eq([3, 4, 5, 18, 34])
expect(layers).to eq([alt_background, bg_item, trinket, alt_body, alt_head])
# alt_background (3), bg_item (4), alt_body (18), alt_head (34), trinket (46)
expect(layers.map(&:depth)).to eq([3, 4, 18, 34, 46])
expect(layers).to eq([alt_background, bg_item, alt_body, alt_head, trinket])
end
end
end