Files
sanspie a46347e2c6
ci-platforms / darwin arm64 smoke (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Failing after 12m19s
ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
github-native / gitea-pending (push) Skipped
github-native / darwin amd64 native (push) Skipped
github-native / linux amd64 native (push) Skipped
github-native / windows amd64 native (push) Skipped
github-native / darwin arm64 native (push) Skipped
github-native / gitea-result (push) Skipped
ci / checks (push) Failing after 23m57s
ci / release (push) Skipped
GitHub / windows amd64 native GitHub-hosted native CI: failure
GitHub / darwin arm64 native GitHub-hosted native CI: failure
GitHub / darwin amd64 native GitHub-hosted native CI: failure
GitHub / linux amd64 native GitHub-hosted native CI: success
editor: add art studio and stage 12.5 workflows
2026-11-08 18:47:57 +03:00

139 lines
5.1 KiB
Go

package artedit
import (
"bytes"
"context"
"image"
"os"
"path/filepath"
"testing"
"aego/art"
"aego/asset"
"aego/core/ids"
"aego/editor/core/document"
)
func pendingArtRecovery(t *testing.T) (*document.Manager, *document.AssetDocument, *Document, string, []byte) {
t.Helper()
root := t.TempDir()
if err := os.MkdirAll(filepath.Join(root, "assets"), 0755); err != nil {
t.Fatal(err)
}
d, err := art.New(8, 8, art.RGBA, art.DefaultLimits())
if err != nil {
t.Fatal(err)
}
data, err := d.Encode()
d.Close()
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(root, "assets", "sample.aeart"), data, 0644); err != nil {
t.Fatal(err)
}
id := ids.NewAssetID()
open := func() (*document.Manager, *document.AssetDocument, *Document) {
t.Helper()
m := document.NewManager(document.Options{Root: root})
if err := m.Handlers().Register(document.Handler{ID: "art", Kinds: []asset.ArtifactKind{asset.ArtTexture}, Extensions: []string{".aeart"}, Capabilities: document.View | document.EditSource}); err != nil {
t.Fatal(err)
}
a, err := m.OpenAsset(document.AssetRequest{Asset: id, Path: "assets/sample.aeart", Kind: asset.ArtTexture})
if err != nil {
t.Fatal(err)
}
b, err := Bind(m, a)
if err != nil {
t.Fatal(err)
}
return m, a, b
}
old, a, b := open()
if _, err := b.Execute(context.Background(), art.Operation{Kind: "stroke", Points: []image.Point{{X: 1, Y: 1}}, Brush: art.Brush{Size: 1, Paint: art.Paint{Color: art.Color{R: 255, A: 255}}}}); err != nil {
t.Fatal(err)
}
if err := old.WriteAssetRecovery(a); err != nil {
t.Fatal(err)
}
file := filepath.Join(root, document.RecoveryDir, "asset-"+id.String()+".recovery")
before, err := os.ReadFile(file)
if err != nil {
t.Fatal(err)
}
// A second manager represents a fresh process; the first is deliberately
// not closed as though it had terminated without cleanup.
m, assetDoc, model := open()
if !model.PendingRecovery {
t.Fatal("recovery was not offered")
}
t.Cleanup(b.Close)
return m, assetDoc, model, file, before
}
func TestPendingArtRecoveryCannotBeOverwrittenByNewEdits(t *testing.T) {
m, _, b, file, before := pendingArtRecovery(t)
defer b.Close()
if _, err := b.Execute(context.Background(), art.Operation{Kind: "stroke", Points: []image.Point{{X: 2, Y: 2}}, Brush: art.Brush{Size: 1, Paint: art.Paint{Color: art.Color{G: 255, A: 255}}}}); err == nil {
t.Fatal("editing was allowed before resolving the previous recovery")
}
// A direct session caller could still dirty the model; the recovery writer
// itself must guard the old copy even when normal editor actions are blocked.
if _, err := b.Session.Execute(context.Background(), art.Operation{Kind: "stroke", Points: []image.Point{{X: 2, Y: 2}}, Brush: art.Brush{Size: 1, Paint: art.Paint{Color: art.Color{G: 255, A: 255}}}}); err != nil {
t.Fatal(err)
}
if err := m.WriteAssetRecovery(b.Asset); err == nil {
t.Fatal("a periodic recovery could overwrite the pending recovery")
}
if err := b.Save(); err == nil {
t.Fatal("saving was allowed before resolving the previous recovery")
}
if got, err := os.ReadFile(file); err != nil || !bytes.Equal(got, before) {
t.Fatal("the pending recovery was changed")
}
if err := b.Recover(); err != nil || !b.Asset.Dirty() || b.PendingRecovery {
t.Fatalf("explicit recovery failed: %v", err)
}
if err := b.Save(); err != nil {
t.Fatalf("saving the explicitly recovered drawing failed: %v", err)
}
if _, err := os.Stat(file); !os.IsNotExist(err) {
t.Fatalf("explicitly saved recovery was retained: %v", err)
}
}
func TestClosingOrDiscardingAnUnrelatedEditPreservesPendingArtRecovery(t *testing.T) {
m, d, b, file, before := pendingArtRecovery(t)
// Simulate an unexpected dirty session even though normal art edits are
// blocked, then exercise the actual Discard-on-Quit cleanup path.
if _, err := b.Session.Execute(context.Background(), art.Operation{Kind: "stroke", Points: []image.Point{{X: 2, Y: 2}}, Brush: art.Brush{Size: 1, Paint: art.Paint{Color: art.Color{G: 255, A: 255}}}}); err != nil {
t.Fatal(err)
}
if err := m.DiscardOpenRecovery(); err != nil {
t.Fatal(err)
}
if got, err := os.ReadFile(file); err != nil || !bytes.Equal(got, before) {
t.Fatal("discard-on-quit erased an unresolved recovery")
}
if err := m.CloseAsset(d.Asset, true); err != nil {
t.Fatal(err)
}
if got, err := os.ReadFile(file); err != nil || !bytes.Equal(got, before) {
t.Fatal("closing the current asset erased an unresolved recovery")
}
}
func TestExplicitDiscardOfPendingArtRecoveryAllowsEditing(t *testing.T) {
_, _, b, file, _ := pendingArtRecovery(t)
defer b.Close()
if err := b.DiscardRecovery(); err != nil || b.PendingRecovery {
t.Fatalf("explicit discard failed: %v", err)
}
if _, err := os.Stat(file); !os.IsNotExist(err) {
t.Fatalf("explicitly discarded recovery still exists: %v", err)
}
if _, err := b.Execute(context.Background(), art.Operation{Kind: "stroke", Points: []image.Point{{X: 2, Y: 2}}, Brush: art.Brush{Size: 1, Paint: art.Paint{Color: art.Color{G: 255, A: 255}}}}); err != nil {
t.Fatalf("editing stayed blocked after explicit discard: %v", err)
}
}