forked from OpenNeo/impress
Add image comparison tests for OutfitImageRenderer
This commit is contained in:
parent
55fa50c22a
commit
aa45ea17b3
6 changed files with 102 additions and 52 deletions
BIN
spec/fixtures/outfit_images/Blue Acara With Hat.png
vendored
Normal file
BIN
spec/fixtures/outfit_images/Blue Acara With Hat.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 91 KiB |
BIN
spec/fixtures/outfit_images/Blue Acara.png
vendored
Normal file
BIN
spec/fixtures/outfit_images/Blue Acara.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
BIN
spec/fixtures/outfit_images/Hat.png
vendored
Normal file
BIN
spec/fixtures/outfit_images/Hat.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
54
spec/fixtures/zones.yml
vendored
54
spec/fixtures/zones.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue