Files
archvillainette cdbbba3181
test-image / build-image (push) Successful in 46s
test / test (push) Successful in 1m26s
build-binaries / build-binaries (push) Successful in 2m20s
build-image / publish (push) Successful in 31s
add content-addressed hak source mode (#7)
Reviewed-on: #7
Co-authored-by: vickydotbat <vickydotbat@tutamail.com>
Co-committed-by: vickydotbat <vickydotbat@tutamail.com>
2026-06-19 20:56:36 +00:00

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))
}
}