250 lines
7.3 KiB
Go
250 lines
7.3 KiB
Go
package pipeline
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.westgate.pw/ShadowsOverWestgate/sow-tools/internal/buildinfo"
|
|
"git.westgate.pw/ShadowsOverWestgate/sow-tools/internal/erf"
|
|
"git.westgate.pw/ShadowsOverWestgate/sow-tools/internal/project"
|
|
)
|
|
|
|
const directProjectConfig = `{
|
|
"module": {
|
|
"name": "Test Module",
|
|
"resref": "testmod",
|
|
"hak_order": ["core_01"]
|
|
},
|
|
"paths": {
|
|
"source": "src",
|
|
"assets": "assets",
|
|
"build": "build"
|
|
}
|
|
}
|
|
`
|
|
|
|
func writeBlobAt(t *testing.T, root, sha, content string) {
|
|
t.Helper()
|
|
dir := filepath.Join(root, "sha256", sha[0:2], sha[2:4])
|
|
mustMkdir(t, dir)
|
|
mustWriteFile(t, filepath.Join(dir, sha), content)
|
|
}
|
|
|
|
func writeBlob(t *testing.T, root string, payload []byte) string {
|
|
t.Helper()
|
|
sum := sha256.Sum256(payload)
|
|
sha := hex.EncodeToString(sum[:])
|
|
writeBlobAt(t, root, sha, string(payload))
|
|
return sha
|
|
}
|
|
|
|
func writeDirectManifest(t *testing.T, root string, sources map[string]SourceAsset, assets []string) string {
|
|
t.Helper()
|
|
manifest := SourceBuildManifest{
|
|
Schema: 1,
|
|
BuilderID: buildinfo.String(),
|
|
ModuleHAKs: []string{"core_01"},
|
|
HAKs: []SourceManifestHAK{{
|
|
Name: "core_01",
|
|
Group: "core",
|
|
Priority: 1,
|
|
MaxBytes: 1 << 20,
|
|
Assets: assets,
|
|
}},
|
|
AssetSources: sources,
|
|
}
|
|
raw, err := json.Marshal(manifest)
|
|
if err != nil {
|
|
t.Fatalf("marshal manifest: %v", err)
|
|
}
|
|
path := filepath.Join(root, "build-source-manifest.json")
|
|
mustWriteFile(t, path, string(raw))
|
|
return path
|
|
}
|
|
|
|
func loadDirectProject(t *testing.T, root string) *project.Project {
|
|
t.Helper()
|
|
mustWriteFile(t, filepath.Join(root, "nwn-tool.json"), directProjectConfig)
|
|
mustMkdir(t, filepath.Join(root, "build"))
|
|
p, err := project.Load(root)
|
|
if err != nil {
|
|
t.Fatalf("load project: %v", err)
|
|
}
|
|
return p
|
|
}
|
|
|
|
func TestBuildHAKsFromContentAddressedManifestWithoutAssetsTree(t *testing.T) {
|
|
root := t.TempDir()
|
|
p := loadDirectProject(t, root)
|
|
|
|
blobs := filepath.Join(root, "blobs")
|
|
payload := []byte("tga-payload-bytes")
|
|
sha := writeBlob(t, blobs, payload)
|
|
manifestPath := writeDirectManifest(t, root,
|
|
map[string]SourceAsset{"core/a.tga": {SHA256: sha, SizeBytes: int64(len(payload))}},
|
|
[]string{"core/a.tga"})
|
|
|
|
result, err := BuildHAKsWithOptions(p, BuildHAKOptions{
|
|
SourceManifestPath: manifestPath,
|
|
ContentAddressedRoot: blobs,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("build direct haks: %v", err)
|
|
}
|
|
if result.HAKAssets != 1 {
|
|
t.Fatalf("hak assets = %d, want 1", result.HAKAssets)
|
|
}
|
|
|
|
if _, err := os.Stat(filepath.Join(root, "assets")); !errors.Is(err, os.ErrNotExist) {
|
|
t.Fatalf("direct build must not materialize an assets tree (stat err = %v)", err)
|
|
}
|
|
|
|
hakPath := filepath.Join(root, "build", "core_01.hak")
|
|
raw, err := os.ReadFile(hakPath)
|
|
if err != nil {
|
|
t.Fatalf("read hak: %v", err)
|
|
}
|
|
archive, err := erf.Read(bytes.NewReader(raw))
|
|
if err != nil {
|
|
t.Fatalf("decode hak: %v", err)
|
|
}
|
|
if len(archive.Resources) != 1 {
|
|
t.Fatalf("hak resources = %d, want 1", len(archive.Resources))
|
|
}
|
|
if got := string(archive.Resources[0].Data); got != string(payload) {
|
|
t.Fatalf("resource payload = %q, want %q", got, payload)
|
|
}
|
|
if archive.Resources[0].Name != "a" {
|
|
t.Fatalf("resref = %q, want a", archive.Resources[0].Name)
|
|
}
|
|
}
|
|
|
|
func TestBuildHAKsFromContentAddressedManifestRejectsMissingRoot(t *testing.T) {
|
|
root := t.TempDir()
|
|
p := loadDirectProject(t, root)
|
|
|
|
blobs := filepath.Join(root, "blobs")
|
|
payload := []byte("tga-payload-bytes")
|
|
sha := writeBlob(t, blobs, payload)
|
|
manifestPath := writeDirectManifest(t, root,
|
|
map[string]SourceAsset{"core/a.tga": {SHA256: sha, SizeBytes: int64(len(payload))}},
|
|
[]string{"core/a.tga"})
|
|
|
|
_, err := BuildHAKsWithOptions(p, BuildHAKOptions{SourceManifestPath: manifestPath})
|
|
if err == nil {
|
|
t.Fatal("expected failure when content-addressed root is missing")
|
|
}
|
|
}
|
|
|
|
func TestBuildHAKsFromContentAddressedManifestRejectsMissingBlob(t *testing.T) {
|
|
root := t.TempDir()
|
|
p := loadDirectProject(t, root)
|
|
|
|
blobs := filepath.Join(root, "blobs")
|
|
sha := strings.Repeat("b", 64)
|
|
manifestPath := writeDirectManifest(t, root,
|
|
map[string]SourceAsset{"core/a.tga": {SHA256: sha, SizeBytes: 4}},
|
|
[]string{"core/a.tga"})
|
|
|
|
_, err := BuildHAKsWithOptions(p, BuildHAKOptions{
|
|
SourceManifestPath: manifestPath,
|
|
ContentAddressedRoot: blobs,
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected failure for missing source blob")
|
|
}
|
|
}
|
|
|
|
func TestBuildHAKsFromContentAddressedManifestRejectsCorruptBlob(t *testing.T) {
|
|
root := t.TempDir()
|
|
p := loadDirectProject(t, root)
|
|
|
|
blobs := filepath.Join(root, "blobs")
|
|
declared := sha256.Sum256([]byte("abcd"))
|
|
sha := hex.EncodeToString(declared[:])
|
|
// Same length, different bytes: size check passes, content hash mismatches.
|
|
writeBlobAt(t, blobs, sha, "wxyz")
|
|
manifestPath := writeDirectManifest(t, root,
|
|
map[string]SourceAsset{"core/a.tga": {SHA256: sha, SizeBytes: 4}},
|
|
[]string{"core/a.tga"})
|
|
|
|
_, err := BuildHAKsWithOptions(p, BuildHAKOptions{
|
|
SourceManifestPath: manifestPath,
|
|
ContentAddressedRoot: blobs,
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected sha256 mismatch failure for corrupt blob")
|
|
}
|
|
if !strings.Contains(err.Error(), "sha256 mismatch") {
|
|
t.Fatalf("error = %v, want sha256 mismatch", err)
|
|
}
|
|
if _, statErr := os.Stat(filepath.Join(root, "build", "core_01.hak")); !errors.Is(statErr, os.ErrNotExist) {
|
|
t.Fatalf("corrupt build must not leave a completed hak (stat err = %v)", statErr)
|
|
}
|
|
}
|
|
|
|
func TestLegacySourceManifestStillBuildsFromAssetsTree(t *testing.T) {
|
|
root := t.TempDir()
|
|
mustMkdir(t, filepath.Join(root, "src", "module"))
|
|
mustMkdir(t, filepath.Join(root, "assets", "core"))
|
|
mustMkdir(t, filepath.Join(root, "build"))
|
|
mustWriteFile(t, filepath.Join(root, "nwn-tool.json"), `{
|
|
"module": {"name": "Test Module", "resref": "testmod", "hak_order": ["group:core"]},
|
|
"paths": {"source": "src", "assets": "assets", "build": "build"},
|
|
"haks": [{"name": "core", "priority": 1, "include": ["core/**"]}]
|
|
}
|
|
`)
|
|
mustWriteFile(t, filepath.Join(root, "src", "module", "module.ifo.json"), `{
|
|
"file_type": "IFO ",
|
|
"file_version": "V3.2",
|
|
"root": {"struct_type": 0, "fields": [
|
|
{"label": "Mod_Name", "type": "CExoString", "value": "Test Module"},
|
|
{"label": "Mod_HakList", "type": "List", "value": []}
|
|
]}
|
|
}
|
|
`)
|
|
mustWriteFile(t, filepath.Join(root, "assets", "core", "a.tga"), strings.Repeat("a", 16))
|
|
mustWriteFile(t, filepath.Join(root, "assets", "core", "b.tga"), strings.Repeat("b", 16))
|
|
|
|
manifestPath := filepath.Join(root, "legacy-source-manifest.json")
|
|
mustWriteFile(t, manifestPath, `{
|
|
"module_haks": ["core"],
|
|
"haks": [{"name": "core", "group": "core", "priority": 1, "assets": ["core/a.tga", "core/b.tga"]}]
|
|
}
|
|
`)
|
|
|
|
p, err := project.Load(root)
|
|
if err != nil {
|
|
t.Fatalf("load project: %v", err)
|
|
}
|
|
if err := p.Scan(); err != nil {
|
|
t.Fatalf("scan: %v", err)
|
|
}
|
|
|
|
result, err := BuildHAKsWithOptions(p, BuildHAKOptions{SourceManifestPath: manifestPath})
|
|
if err != nil {
|
|
t.Fatalf("legacy build: %v", err)
|
|
}
|
|
if len(result.HAKPaths) != 1 {
|
|
t.Fatalf("hak paths = %d, want 1", len(result.HAKPaths))
|
|
}
|
|
raw, err := os.ReadFile(filepath.Join(root, "build", "core.hak"))
|
|
if err != nil {
|
|
t.Fatalf("read legacy hak: %v", err)
|
|
}
|
|
archive, err := erf.Read(bytes.NewReader(raw))
|
|
if err != nil {
|
|
t.Fatalf("decode legacy hak: %v", err)
|
|
}
|
|
if len(archive.Resources) != 2 {
|
|
t.Fatalf("legacy hak resources = %d, want 2", len(archive.Resources))
|
|
}
|
|
}
|