// End-to-end check of the capture pipeline, minus webOS. // // Runs the engine with the `tone` backend feeding the TCP and HTTP sinks, then // connects to both as a client and verifies that real audio comes out with the // right framing. Everything here works identically on the TV; only the Luna // layer is missing, so this exercises capture -> DSP -> fan-out -> socket. // // Build and run: test/run-tests.sh #include "common/log.h" #include "engine.h" #include #include #include #include #include #include #include #include #include #include #include #define HTTP_PORT 45812 #define TCP_PORT 45811 static int failures = 0; static void check(bool ok, const char* what) { printf("%s %s\n", ok ? " ok " : " FAIL", what); if (!ok) failures++; } static void sleep_ms(int ms) { struct timespec ts = { .tv_sec = ms / 1000, .tv_nsec = (long)(ms % 1000) * 1000000L }; nanosleep(&ts, NULL); } static int connect_local(int port) { int fd = socket(AF_INET, SOCK_STREAM, 0); if (fd < 0) return -1; struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons((uint16_t)port); addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); if (connect(fd, (struct sockaddr*)&addr, sizeof(addr)) != 0) { close(fd); return -1; } struct timeval tv = { .tv_sec = 3, .tv_usec = 0 }; setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); return fd; } // Reads exactly `len` bytes or fails. static bool read_exact(int fd, void* dst, size_t len) { size_t got = 0; while (got < len) { ssize_t n = read(fd, (char*)dst + got, len - got); if (n <= 0) return false; got += (size_t)n; } return true; } static uint32_t rd_u32le(const uint8_t* p) { return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24); } static uint16_t rd_u16le(const uint8_t* p) { return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8)); } // True if the buffer contains something other than digital silence. static bool has_signal(const int16_t* pcm, size_t samples) { for (size_t i = 0; i < samples; i++) { if (pcm[i] > 500 || pcm[i] < -500) return true; } return false; } static void test_http(void) { printf("HTTP WAV sink\n"); int fd = connect_local(HTTP_PORT); if (fd < 0) { check(false, "connect to the HTTP sink"); return; } const char* req = "GET /audio.wav HTTP/1.1\r\nHost: tv\r\n\r\n"; check(write(fd, req, strlen(req)) == (ssize_t)strlen(req), "send the request"); // Headers end at the blank line; read a byte at a time so we do not eat // into the WAV header that follows. char headers[1024]; size_t hlen = 0; bool complete = false; while (hlen < sizeof(headers) - 1) { if (!read_exact(fd, headers + hlen, 1)) break; hlen++; headers[hlen] = '\0'; if (hlen >= 4 && memcmp(headers + hlen - 4, "\r\n\r\n", 4) == 0) { complete = true; break; } } check(complete, "receive complete HTTP headers"); check(strstr(headers, "200 OK") != NULL, "status line is 200 OK"); check(strstr(headers, "Content-Type: audio/wav") != NULL, "content type is audio/wav"); uint8_t wav[44]; if (!read_exact(fd, wav, sizeof(wav))) { check(false, "receive the WAV header"); close(fd); return; } check(memcmp(wav, "RIFF", 4) == 0, "RIFF magic"); check(memcmp(wav + 8, "WAVE", 4) == 0, "WAVE magic"); check(memcmp(wav + 12, "fmt ", 4) == 0, "fmt chunk"); check(rd_u32le(wav + 16) == 16, "fmt chunk length is 16"); check(rd_u16le(wav + 20) == 1, "format is PCM"); check(rd_u16le(wav + 22) == 2, "2 channels"); check(rd_u32le(wav + 24) == 48000, "48000 Hz"); check(rd_u32le(wav + 28) == 48000 * 4, "byte rate matches"); check(rd_u16le(wav + 32) == 4, "block align is 4"); check(rd_u16le(wav + 34) == 16, "16 bits per sample"); check(memcmp(wav + 36, "data", 4) == 0, "data chunk"); check(rd_u32le(wav + 4) == 0xFFFFFFFFu, "RIFF size is the unknown-length marker"); check(rd_u32le(wav + 40) == 0xFFFFFFFFu, "data size is the unknown-length marker"); int16_t pcm[4096]; bool got = read_exact(fd, pcm, sizeof(pcm)); check(got, "receive 16 KB of audio"); check(got && has_signal(pcm, sizeof(pcm) / sizeof(pcm[0])), "audio is not silence"); close(fd); } static void test_tcp(void) { printf("Raw PCM TCP sink\n"); int fd = connect_local(TCP_PORT); if (fd < 0) { check(false, "connect to the TCP sink"); return; } int16_t pcm[4096]; bool got = read_exact(fd, pcm, sizeof(pcm)); check(got, "receive 16 KB of audio"); check(got && has_signal(pcm, sizeof(pcm) / sizeof(pcm[0])), "audio is not silence"); close(fd); } static void test_status(engine_t* e) { printf("Status document\n"); json_writer_t w; jw_init(&w); jw_obj_open(&w, NULL); engine_write_status(e, &w); jw_obj_close(&w); char* text = jw_take(&w); check(text != NULL, "status serialises"); if (!text) return; json_value_t* v = json_parse(text); check(v != NULL, "status is valid JSON"); if (v) { check(strcmp(json_str(v, "state", ""), "running") == 0, "state is running"); const json_value_t* cap = json_get(v, "capture"); check(strcmp(json_str(cap, "backend", ""), "tone") == 0, "backend is the tone generator"); check(json_int(cap, "rate", 0) == 48000, "reports 48000 Hz"); check(json_int(cap, "frames", 0) > 0, "frames have been captured"); const json_value_t* levels = json_get(v, "levels"); check(json_num(levels, "peak", 0) > 0.05, "peak level is non-trivial"); check(json_len(json_get(levels, "bands")) == DSP_BANDS, "all bands reported"); const json_value_t* sinks = json_get(v, "sinks"); check(json_len(sinks) == 3, "three sinks reported"); for (size_t i = 0; i < json_len(sinks); i++) { const json_value_t* s = json_at(sinks, i); char label[64]; snprintf(label, sizeof(label), "sink '%s' started cleanly", json_str(s, "id", "?")); check(json_bool(s, "ok", false), label); if (strcmp(json_str(s, "id", ""), "hyperhdrAdjust") == 0) { // A host has no Luna bus (see lunaservice_stub.c), so the // sink can never confirm the restricted app is foreground. // The correct failure mode is inactive, not "assume yes". check(strcmp(json_str(s, "restrictToApp", ""), "some.other.app") == 0, "restriction target reported back"); check(json_bool(s, "restrictedAppActive", true) == false, "restricted app correctly reported as not active (fail-closed)"); } } json_free(v); } free(text); } int main(void) { log_init(LOG_WARN); // keep the test output readable char cfg_text[512]; snprintf(cfg_text, sizeof(cfg_text), "{\"capture\":{\"backend\":\"tone\",\"rate\":48000,\"channels\":2}," "\"sinks\":[\"tcp\",\"http\",\"hyperhdrAdjust\"]," "\"tcp\":{\"port\":%d}," "\"http\":{\"port\":%d}," "\"hyperhdrAdjust\":{\"host\":\"127.0.0.1\",\"port\":19444," "\"restrictToApp\":\"some.other.app\"}}", TCP_PORT, HTTP_PORT); json_value_t* cfg = json_parse(cfg_text); if (!cfg) { fprintf(stderr, "test bug: config does not parse\n"); return 1; } engine_t* e = engine_create(NULL, NULL); if (!e) { fprintf(stderr, "cannot create the engine\n"); return 1; } char err[256] = { 0 }; printf("Engine\n"); if (!engine_start(e, cfg, err, sizeof(err))) { printf(" FAIL start: %s\n", err); return 1; } // Startup happens on the engine thread; wait for it to settle. for (int i = 0; i < 100 && engine_state(e) == ENGINE_STARTING; i++) sleep_ms(50); check(engine_state(e) == ENGINE_RUNNING, "engine reaches the running state"); if (engine_state(e) != ENGINE_RUNNING) { engine_destroy(e); json_free(cfg); return 1; } test_tcp(); test_http(); test_status(e); engine_stop(e); check(engine_state(e) == ENGINE_STOPPED, "engine stops cleanly"); engine_destroy(e); json_free(cfg); printf("\n%s\n", failures ? "FAILED" : "All engine checks passed."); return failures ? 1 : 0; }