1
0
Fork 0
forked from hanez/xtc

Phase 28: add project diagnostics, dependency inspection and durability regressions

This commit is contained in:
Johannes Findeisen 2026-10-27 16:58:26 +01:00
commit ca6c869b0b
16 changed files with 495 additions and 42 deletions

View file

@ -3,7 +3,7 @@
Store small, redistributable fixtures here. Document their origin and license.
Do not commit large recordings or assets with unclear licensing.
`project-v1.xtc` through `project-v9.xtc` are hand-authored xtc metadata fixtures
`project-v1.xtc` through `project-v10.xtc` are hand-authored xtc metadata fixtures
created for this repository, under the project license. They contain no audio
or third-party resources. The v5/v6 plugin fixtures intentionally reference a
missing module to exercise metadata migration without loading code.

39
tests/fixtures/project-v10.xtc vendored Normal file
View file

@ -0,0 +1,39 @@
XTC_PROJECT 10
APP 0 1 0
TEMPO 120000
IDS 1 1
GRAPH 1 2 2 1
NODE 0 1 3f000000
NODE 1 4 00000000
EDGE 0 1
TRACKS 1
TRACK 0 4b657973
MIX 3f800000 00000000 0 0
ROUTE 0 18446744073709551615
CLIPS 1
CLIP 0 0 0 960 0 960 5665727365
ASSET -
MIDI 1
AUDIO 3f800000 0 0
NOTES 1 1
NOTE 0 0 0 480 69 100 0
AUTOMATION 1
POINT 0 0 0 3f800000 0
CONTROL_MAPS 16
MAP 0 7 10 64
MAP 1 7 10 64
MAP 2 7 10 64
MAP 3 7 10 64
MAP 4 7 10 64
MAP 5 7 10 64
MAP 6 7 10 64
MAP 7 7 10 64
MAP 8 7 10 64
MAP 9 7 10 64
MAP 10 7 10 64
MAP 11 7 10 64
MAP 12 7 10 64
MAP 13 7 10 64
MAP 14 7 10 64
MAP 15 7 10 64
END

View file

@ -19,7 +19,7 @@ int main(void)
assert(mkdtemp(root));
assert(xtc_project_create(&p) == XTC_OK);
snprintf(file,sizeof(file),"%s/migration.xtc",root);
for (version = 1; version <= 9; ++version) {
for (version = 1; version <= 10; ++version) {
xtc_track_info t; xtc_clip_info c; size_t notes, points;
snprintf(old,sizeof(old),"tests/fixtures/project-v%u.xtc",version);
assert(xtc_project_load(p,old) == XTC_OK);
@ -89,6 +89,6 @@ int main(void)
assert(xtc_project_load(p,file) == XTC_ERR_FORMAT);
{ xtc_clip_info c; assert(xtc_project_find_clip(p,clip,&c) == XTC_OK && c.asset_path[0]); } }
xtc_project_destroy(p); unlink(file); rmdir(moved);
puts("v1-v9 migration, current folder relocation, cwd independence and missing-media preservation passed.");
puts("v1-v10 migration, current folder relocation, cwd independence and missing-media preservation passed.");
return 0;
}

View file

@ -0,0 +1,52 @@
#!/usr/bin/env python3
"""Metadata diagnostics and dependency inspection without a server or modules."""
from pathlib import Path
import subprocess
import sys
import tempfile
binary = str(Path(sys.argv[1]).resolve())
def check(option, path, code, *messages):
result = subprocess.run([binary, option, str(path)], capture_output=True, text=True)
assert result.returncode == code, (result.returncode, result.stdout, result.stderr)
for message in messages:
assert message in result.stdout + result.stderr, (message, result.stdout, result.stderr)
base = Path("tests/fixtures/project-v10.xtc").read_text()
check("--inspect", "tests/fixtures/project-v10.xtc", 0, "Project format 10", "missing/unreadable: 0")
with tempfile.TemporaryDirectory(prefix="xtc-inspect-") as directory:
root = Path(directory)
project = root / "project.xtc"
media = root / "media.wav"
# Relative paths resolve against the project, independently of the subprocess cwd.
audio = base.replace("ASSET -", "ASSET " + b"media.wav".hex()).replace("MIDI 1", "MIDI 0")
audio = audio.replace("NOTES 1 1\nNOTE 0 0 0 480 69 100 0", "NOTES 0 0")
project.write_text(audio)
check("--validate", project, 0, "metadata valid")
check("--inspect", project, 2, "Media clip 0", str(media), "missing/unreadable: 1")
media.write_text("Deliberately not WAV data")
check("--inspect", project, 0, "available", "WAV contents", "not checked")
module = root / "module.so"
plugin = Path("tests/fixtures/project-plugin-v6.xtc").read_text()
plugin = plugin.replace(b"/missing/test.so".hex(), str(module).encode().hex())
project.write_text(plugin)
check("--validate", project, 0)
check("--inspect", project, 2, "LADSPA node 0 ID 900000 descriptor 0", "xtc_test_gain", str(module))
# Inspection succeeds for readable files without trying dlopen or decoding.
module.write_text("Deliberately not an executable module")
check("--inspect", project, 0, "no plugin code executed")
for text, messages in [
(base.replace("XTC_PROJECT 10", "XTC_PROJECT 999"), ("line 1", "999", "1..10")),
("XTC_PROJECT 10\n", ("line 2", "APP")),
(base.replace("TEMPO 120000", "TEMPO 0"), ("line 3", "TEMPO")),
(base + "OPTIONAL_FUTURE 1\n", ("Unknown fields",)),
(base.replace("APP 0 1 0", "APP 0 1 0 extra"), ("APP",)),
]:
project.write_text(text)
check("--inspect", project, 1, *messages)
with project.open("wb") as stream:
stream.truncate(4194305)
check("--inspect", project, 1, "SIZE", "4 MiB")
check("--inspect", root / "absent.xtc", 1, "OPEN")
print("Project inspection availability, diagnostics and strict compatibility passed.")

View file

@ -0,0 +1,112 @@
#define _POSIX_C_SOURCE 200809L
#include "xtc/xtc_project.h"
#include <assert.h>
#include <dirent.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
/* Linker interposition injects errors only into this test executable. */
static int fault;
int __real_fflush(FILE *file);
int __real_fclose(FILE *file);
int __real_fsync(int descriptor);
int __real_rename(const char *old, const char *next);
int __wrap_fflush(FILE *file)
{
if (fault == 1) { errno = EIO; return EOF; }
return __real_fflush(file);
}
int __wrap_fclose(FILE *file)
{
int result = __real_fclose(file);
if (fault == 4) { errno = EIO; return EOF; }
return result;
}
int __wrap_fsync(int descriptor)
{
struct stat info;
assert(fstat(descriptor,&info) == 0);
if ((fault == 2 && S_ISREG(info.st_mode)) || (fault == 5 && S_ISDIR(info.st_mode))) { errno = EIO; return -1; }
return __real_fsync(descriptor);
}
int __wrap_rename(const char *old, const char *next)
{
if (fault == 3) { errno = EIO; return -1; }
return __real_rename(old,next);
}
static void write_text(const char *path, const char *text)
{
FILE *file = fopen(path,"wb"); assert(file);
assert(fwrite(text,1,strlen(text),file) == strlen(text)); assert(fclose(file) == 0);
}
static size_t bytes(const char *path, char out[4096])
{
FILE *file = fopen(path,"rb"); size_t n;
assert(file); n = fread(out,1,4096,file); assert(!ferror(file) && feof(file)); assert(fclose(file) == 0); return n;
}
static void no_temporaries(const char *path)
{
DIR *directory = opendir(path); struct dirent *entry; assert(directory);
while ((entry = readdir(directory))) assert(!strstr(entry->d_name,".tmp."));
assert(closedir(directory) == 0);
}
int main(void)
{
char root[] = "/tmp/xtc-project-report-XXXXXX", song[256], bad[256];
char before[4096], after[4096]; size_t size, n;
xtc_project *old = NULL, *next = NULL, *loaded = NULL;
xtc_project_diagnostic diagnostic;
xtc_track_info track;
uint64_t id;
int mode;
assert(mkdtemp(root));
snprintf(song,sizeof(song),"%s/song.xtc",root); snprintf(bad,sizeof(bad),"%s/bad.xtc",root);
assert(xtc_project_create(&old) == XTC_OK && xtc_project_create(&next) == XTC_OK && xtc_project_create(&loaded) == XTC_OK);
assert(xtc_project_add_track(old,"Old",&id) == XTC_OK && xtc_project_add_track(next,"New",&id) == XTC_OK);
assert(xtc_project_save(old,song) == XTC_OK); size = bytes(song,before);
assert(xtc_project_load_diagnostic(loaded,song,&diagnostic) == XTC_OK && diagnostic.result == XTC_OK &&
diagnostic.format_version == 10 && !diagnostic.line);
for (mode = 1; mode <= 5; ++mode) {
fault = mode;
assert(xtc_project_save(next,song) == XTC_ERR_IO);
fault = 0;
no_temporaries(root);
n = bytes(song,after);
if (mode < 5) assert(n == size && !memcmp(before,after,size));
assert(xtc_project_load(loaded,song) == XTC_OK);
assert(xtc_project_get_track(loaded,0,&track) == XTC_OK && !strcmp(track.name,mode < 5 ? "Old" : "New"));
}
write_text(bad,"XTC_PROJECT 999\n");
assert(xtc_project_load_diagnostic(loaded,bad,&diagnostic) == XTC_ERR_UNSUPPORTED && diagnostic.line == 1 &&
diagnostic.format_version == 999 && !strcmp(diagnostic.record,"XTC_PROJECT") && strstr(diagnostic.message,"1..10"));
write_text(bad,"XTC_PROJECT 10\nAPP 0 1 0\nTEMPO nope\n");
assert(xtc_project_load_diagnostic(loaded,bad,&diagnostic) == XTC_ERR_FORMAT && diagnostic.line == 3 &&
!strcmp(diagnostic.record,"TEMPO"));
write_text(bad,"XTC_PROJECT 10\n");
assert(xtc_project_load_diagnostic(loaded,bad,&diagnostic) == XTC_ERR_FORMAT && diagnostic.line == 2 &&
!strcmp(diagnostic.record,"APP"));
write_text(bad,"XTC_PROJECT 10\nAPP 0 1 0\nTEMPO 120000\nIDS 0 0\nGRAPH 1 1 1 0\nNODE 0 7 00000000\n");
assert(xtc_project_load_diagnostic(loaded,bad,&diagnostic) == XTC_ERR_UNSUPPORTED && diagnostic.line == 6 &&
!strcmp(diagnostic.record,"NODE") && strstr(diagnostic.message,"node type"));
{
FILE *file = fopen(bad,"wb"); assert(file);
assert(ftruncate(fileno(file),4194305) == 0 && fclose(file) == 0);
assert(xtc_project_load_diagnostic(loaded,bad,&diagnostic) == XTC_ERR_FORMAT && !diagnostic.line &&
!strcmp(diagnostic.record,"SIZE") && strstr(diagnostic.message,"4 MiB"));
}
{
FILE *file = fopen(bad,"wb"); size_t i; assert(file);
for (i = 0; i < 1536; ++i) assert(fputc('A',file) != EOF);
assert(fputc('\n',file) != EOF && fclose(file) == 0);
assert(xtc_project_load_diagnostic(loaded,bad,&diagnostic) == XTC_ERR_FORMAT && diagnostic.line == 1);
}
assert(xtc_project_get_track(loaded,0,&track) == XTC_OK && !strcmp(track.name,"New"));
assert(xtc_project_load_diagnostic(loaded,"/absent/xtc-report.xtc",&diagnostic) == XTC_ERR_IO && !diagnostic.line);
xtc_project_destroy(old); xtc_project_destroy(next); xtc_project_destroy(loaded);
unlink(song); unlink(bad); assert(rmdir(root) == 0);
puts("Project load diagnostics, retained state, bounded input and pre/post-rename save faults passed.");
return 0;
}