Phase 28: add project diagnostics, dependency inspection and durability regressions
This commit is contained in:
parent
c4daef7c42
commit
ca6c869b0b
16 changed files with 495 additions and 42 deletions
2
tests/fixtures/README.md
vendored
2
tests/fixtures/README.md
vendored
|
|
@ -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
39
tests/fixtures/project-v10.xtc
vendored
Normal 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
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
52
tests/integration/test_project_inspection.py
Normal file
52
tests/integration/test_project_inspection.py
Normal 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.")
|
||||
112
tests/unit/test_project_diagnostics.c
Normal file
112
tests/unit/test_project_diagnostics.c
Normal 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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue