Some checks failed
akbasic CI Build / cmake_build (push) Successful in 2m52s
akbasic CI Build / sanitizers (push) Successful in 3m9s
akbasic CI Build / coverage (push) Failing after 3m12s
akbasic CI Build / akgl_build (push) Failing after 20s
akbasic CI Build / mutation_test (push) Successful in 7m27s
That file wrapped strtoll and strtod because libakstdlib's aksl_ato* family
could not report a conversion failure at all -- atoi("not a number") returned
success with 0, turning four diagnosable errors into wrong answers. Its own note
said what to do when that changed: "When it grows it, delete src/convert.c and
switch the call sites over." 0.2.0 grew it, so it is gone.
Six call sites go straight to the library now: both literal constructors in
src/grammar.c, the line number in src/scanner.c, FunctionVAL in
src/runtime_functions.c, INPUT in src/runtime_commands.c, and GSHAPE's handle in
src/runtime_graphics.c. aksl_strtoll(str, NULL, base, &dest) rather than
aksl_atoll wherever a base is involved, because the ato* forms are base 10 and
grammar.c picks base 8 or 16 off the lexeme's prefix; the NULL endptr is what
makes trailing junk an error rather than a stopping point.
The raised status changed from AKBASIC_ERR_VALUE to AKERR_VALUE, and the message
with it -- VAL("garbage") now says `no digits in "garbage"`. Section 1.8 makes
message text part of the acceptance contract, so that was checked against the
corpus before touching anything: no golden file contained a conversion message,
which is also why section 1.9 had been asking for one. Two exist now, so the next
change to that text has to move a golden file, and one of them pins the
reference's octal-literal defect (section 6 item 10) while it is still
deliberately reproduced.
tests/convert.c became tests/numeric_contract.c rather than being deleted with
the code. The assertions did not stop being worth making when the wrapper went
away -- they became assertions about a contract this port depends on and no
longer owns, and a regression in it would make four things quietly return zero.
Repoints the CI mutation job, which was bounded to src/convert.c and
src/symtab.c. src/symtab.c alone measures 74.1% against the gate of 65.
src/audio_tables.c was measured as a replacement second file and scored 64.7%:
that is a real gap rather than a reason to skip it, since almost every survivor
is in akbasic_audio_state_init, where nothing asserts a freshly initialised audio
state is actually zeroed -- the same gap this job's history records closing for
symtab. Recorded in TODO.md so the file can earn its place back.
One thing worth knowing before reading any score here, and now written down: the
harness's ICR operator only rewrites the constants 0 and 1, so a lookup table of
other values produces no mutants and a high score over one says nothing about
whether its entries are right. tests/audio_verbs.c now asserts all 32 ADSR
entries against the datasheet anyway, because a hand-edit typo is the real
failure mode there -- but that could not and did not move the mutation number,
and claiming otherwise would be the kind of thing this file exists to stop.
72/72 core, 73/73 with libakgl, 72/72 under ASan+UBSan, coverage 93.6% line and
97.8% function, clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
397 lines
16 KiB
C
397 lines
16 KiB
C
/**
|
|
* @file audio_verbs.c
|
|
* @brief Tests the group I verbs and the PLAY queue against the mock backend.
|
|
*
|
|
* Two things here need saying about method. The conversion tables are asserted
|
|
* against numbers derived independently -- the SID clock formula, equal
|
|
* temperament from A440 -- rather than against whatever the code happens to
|
|
* produce, because a table's whole failure mode is being plausibly wrong.
|
|
*
|
|
* And the PLAY queue is driven by hand: akbasic_runtime_settime() then
|
|
* akbasic_runtime_step(), so the test owns the clock the same way a host does.
|
|
* That is the only way to assert that a note is held for its written length
|
|
* without the test sleeping through it.
|
|
*/
|
|
|
|
#include <string.h>
|
|
|
|
#include <akbasic/audio.h>
|
|
#include <akbasic/error.h>
|
|
#include <akbasic/runtime.h>
|
|
|
|
#include "harness.h"
|
|
#include "mockdevice.h"
|
|
#include "testutil.h"
|
|
|
|
/** @brief Bring up a runtime with the mock devices attached and a program loaded. */
|
|
static akerr_ErrorContext AKERR_NOIGNORE *run_program(const char *source)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
PASS(errctx, harness_start(NULL));
|
|
mock_devices_init();
|
|
PASS(errctx, akbasic_runtime_set_devices(&HARNESS_RUNTIME, &MOCK_GRAPHICS,
|
|
&MOCK_AUDIO, &MOCK_INPUT));
|
|
PASS(errctx, akbasic_runtime_load(&HARNESS_RUNTIME, source));
|
|
PASS(errctx, akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
|
|
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 0));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/**
|
|
* @brief The conversion tables, against numbers worked out independently.
|
|
*/
|
|
static void test_tables(void)
|
|
{
|
|
double hz = 0.0;
|
|
int ms = 0;
|
|
|
|
/*
|
|
* The SID computes Fout = Fn * Fclk / 2^24. At Fn = 16777 and an NTSC clock
|
|
* of 1022730 that is 1022.7 Hz, which is the arithmetic and not a reading of
|
|
* the implementation.
|
|
*/
|
|
TEST_REQUIRE_OK(akbasic_audio_register_to_hz(16777, &hz));
|
|
TEST_REQUIRE(hz > 1022.0 && hz < 1023.5, "register 16777 should be about 1022 Hz, got %f", hz);
|
|
TEST_REQUIRE_OK(akbasic_audio_register_to_hz(0, &hz));
|
|
TEST_REQUIRE_FEQ(hz, 0.0);
|
|
TEST_REQUIRE_STATUS(akbasic_audio_register_to_hz(65536, &hz), AKBASIC_ERR_BOUNDS);
|
|
TEST_REQUIRE_STATUS(akbasic_audio_register_to_hz(-1, &hz), AKBASIC_ERR_BOUNDS);
|
|
|
|
/*
|
|
* Every entry of both ADSR tables, not just the ends.
|
|
*
|
|
* Mutation testing is why: checking only rate 0 and rate 15 left the twelve
|
|
* entries between them free to be any number at all, and the failure mode of
|
|
* this file is a constant that is plausibly wrong rather than obviously
|
|
* wrong. A corrupted middle entry makes one envelope decay at the wrong
|
|
* speed, which nothing else in the suite would notice and no listener could
|
|
* attribute.
|
|
*
|
|
* The values are the 6581/8580 datasheet's, transcribed here independently
|
|
* of src/audio_tables.c rather than read back from it.
|
|
*/
|
|
{
|
|
static const int attack[16] = {
|
|
2, 8, 16, 24, 38, 56, 68, 80,
|
|
100, 250, 500, 800, 1000, 3000, 5000, 8000
|
|
};
|
|
int rate = 0;
|
|
|
|
for ( rate = 0; rate < 16; rate++ ) {
|
|
TEST_REQUIRE_OK(akbasic_audio_rate_to_ms(rate, true, &ms));
|
|
TEST_REQUIRE_INT(ms, attack[rate]);
|
|
/*
|
|
* Decay and release are exactly three times attack. That is a
|
|
* property of the chip's envelope generator rather than a
|
|
* coincidence, so it is asserted as the relationship it is.
|
|
*/
|
|
TEST_REQUIRE_OK(akbasic_audio_rate_to_ms(rate, false, &ms));
|
|
TEST_REQUIRE_INT(ms, attack[rate] * 3);
|
|
}
|
|
}
|
|
TEST_REQUIRE_STATUS(akbasic_audio_rate_to_ms(16, true, &ms), AKBASIC_ERR_BOUNDS);
|
|
TEST_REQUIRE_STATUS(akbasic_audio_rate_to_ms(-1, false, &ms), AKBASIC_ERR_BOUNDS);
|
|
TEST_REQUIRE_STATUS(akbasic_audio_rate_to_ms(0, true, NULL), AKERR_NULLPOINTER);
|
|
|
|
/*
|
|
* Equal temperament, checked at the three points anybody would know: A4 is
|
|
* 440, the A an octave up is exactly double, and middle C is 261.63.
|
|
*/
|
|
TEST_REQUIRE_OK(akbasic_audio_note_hz(9, 4, &hz));
|
|
TEST_REQUIRE_FEQ(hz, 440.0);
|
|
TEST_REQUIRE_OK(akbasic_audio_note_hz(9, 5, &hz));
|
|
TEST_REQUIRE_FEQ(hz, 880.0);
|
|
TEST_REQUIRE_OK(akbasic_audio_note_hz(0, 4, &hz));
|
|
TEST_REQUIRE(hz > 261.5 && hz < 261.7, "C4 should be about 261.63 Hz, got %f", hz);
|
|
TEST_REQUIRE_STATUS(akbasic_audio_note_hz(12, 4, &hz), AKBASIC_ERR_BOUNDS);
|
|
TEST_REQUIRE_STATUS(akbasic_audio_note_hz(0, 7, &hz), AKBASIC_ERR_BOUNDS);
|
|
|
|
/* TEMPO 8 should put a quarter note at 500 ms, which is 120 bpm. */
|
|
TEST_REQUIRE_OK(akbasic_audio_whole_note_ms(AKBASIC_TEMPO_DEFAULT, &ms));
|
|
TEST_REQUIRE_INT(ms / 4, 500);
|
|
TEST_REQUIRE_STATUS(akbasic_audio_whole_note_ms(0, &ms), AKBASIC_ERR_BOUNDS);
|
|
TEST_REQUIRE_STATUS(akbasic_audio_whole_note_ms(256, &ms), AKBASIC_ERR_BOUNDS);
|
|
}
|
|
|
|
/** @brief SOUND converts its register and its jiffies, and checks its voice. */
|
|
static void test_sound(void)
|
|
{
|
|
/* 60 jiffies is one second. */
|
|
TEST_REQUIRE_OK(run_program("10 SOUND 1, 16777, 60\n"));
|
|
TEST_REQUIRE(strstr(MOCK.log, "1000ms") != NULL,
|
|
"60 jiffies should be 1000 ms, got \"%s\"", MOCK.log);
|
|
TEST_REQUIRE(strstr(MOCK.log, "tone v0 ") != NULL,
|
|
"BASIC voice 1 should reach backend voice 0, got \"%s\"", MOCK.log);
|
|
harness_stop();
|
|
|
|
TEST_REQUIRE_OK(run_program("10 SOUND 4, 1000, 10\n"));
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "SOUND voice 4") != NULL,
|
|
"voice 4 should be refused, got \"%s\"", HARNESS_OUTPUT);
|
|
harness_stop();
|
|
|
|
/*
|
|
* The frequency sweep is refused rather than faked. Faking it would mean
|
|
* re-issuing tones from step(), which would tie audible pitch to how often
|
|
* the host calls us -- a tune that changes key with the frame rate.
|
|
*/
|
|
TEST_REQUIRE_OK(run_program("10 SOUND 1, 1000, 60, 1, 500, 10\n"));
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "frequency sweep") != NULL,
|
|
"a sweep should be refused, got \"%s\"", HARNESS_OUTPUT);
|
|
harness_stop();
|
|
|
|
/* A sweep direction of zero is "no sweep" and must still play. */
|
|
TEST_REQUIRE_OK(run_program("10 SOUND 1, 1000, 60, 0\n"));
|
|
TEST_REQUIRE(strstr(MOCK.log, "tone v0 ") != NULL,
|
|
"direction 0 is not a sweep and should play, got \"%s\"", MOCK.log);
|
|
harness_stop();
|
|
}
|
|
|
|
/** @brief ENVELOPE maps the SID rate numbers, and sustain is a level not a time. */
|
|
static void test_envelope(void)
|
|
{
|
|
TEST_REQUIRE_OK(run_program("10 ENVELOPE 3, 0, 0, 15, 0, 1\n"));
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.audio_state.envelopes[3].attack, 2);
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.audio_state.envelopes[3].decay, 6);
|
|
TEST_REQUIRE_FEQ(HARNESS_RUNTIME.audio_state.envelopes[3].sustain, 1.0);
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.audio_state.envelopes[3].waveform, AKBASIC_WAVE_SAWTOOTH);
|
|
harness_stop();
|
|
|
|
/* Sustain 0 is silence, and is a legal thing to ask for. */
|
|
TEST_REQUIRE_OK(run_program("10 ENVELOPE 0, 0, 0, 0\n"));
|
|
TEST_REQUIRE_FEQ(HARNESS_RUNTIME.audio_state.envelopes[0].sustain, 0.0);
|
|
harness_stop();
|
|
|
|
TEST_REQUIRE_OK(run_program("10 ENVELOPE 10\n"));
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "ENVELOPE 10") != NULL,
|
|
"preset 10 should be refused, got \"%s\"", HARNESS_OUTPUT);
|
|
harness_stop();
|
|
|
|
TEST_REQUIRE_OK(run_program("10 ENVELOPE 0, 16\n"));
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "ENVELOPE rate 16") != NULL,
|
|
"rate 16 should be refused, got \"%s\"", HARNESS_OUTPUT);
|
|
harness_stop();
|
|
|
|
/* Defining an instrument needs no device; a host may not have lent one yet. */
|
|
TEST_REQUIRE_OK(harness_start(NULL));
|
|
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 ENVELOPE 1, 5\n20 VOL 8\n"));
|
|
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
|
|
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "");
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.audio_state.envelopes[1].attack, 56);
|
|
harness_stop();
|
|
}
|
|
|
|
/** @brief VOL and TEMPO record their setting and range-check it. */
|
|
static void test_vol_and_tempo(void)
|
|
{
|
|
TEST_REQUIRE_OK(run_program("10 VOL 15\n"));
|
|
TEST_REQUIRE_STR(MOCK.log, "vol 1.00\n");
|
|
harness_stop();
|
|
|
|
TEST_REQUIRE_OK(run_program("10 VOL 0\n"));
|
|
TEST_REQUIRE_STR(MOCK.log, "vol 0.00\n");
|
|
harness_stop();
|
|
|
|
TEST_REQUIRE_OK(run_program("10 VOL 16\n"));
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "VOL 16") != NULL,
|
|
"VOL 16 should be refused, got \"%s\"", HARNESS_OUTPUT);
|
|
harness_stop();
|
|
|
|
/*
|
|
* A tempo change rescales the *current* note length rather than resetting it
|
|
* to a quarter note: a program that set eighth notes and then doubled the
|
|
* tempo means faster eighth notes.
|
|
*/
|
|
TEST_REQUIRE_OK(run_program("10 TEMPO 16\n"));
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.audio_state.tempo, 16);
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.audio_state.notems, 250);
|
|
harness_stop();
|
|
|
|
TEST_REQUIRE_OK(run_program("10 TEMPO 0\n"));
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "TEMPO 0") != NULL,
|
|
"TEMPO 0 should be refused, got \"%s\"", HARNESS_OUTPUT);
|
|
harness_stop();
|
|
}
|
|
|
|
/**
|
|
* @brief Bring up a runtime and parse a PLAY string without draining the queue.
|
|
*
|
|
* akbasic_play_parse() is called directly rather than through a program, because
|
|
* running one would also run akbasic_play_service() -- and with the clock left at
|
|
* zero every note's duration has already expired, so the queue would be empty
|
|
* again before the assertions could look at it. That draining is correct
|
|
* behaviour, tested on its own in test_play_is_not_blocking(); here the question
|
|
* is only what a string parses *to*.
|
|
*/
|
|
static akerr_ErrorContext AKERR_NOIGNORE *parse_notes(const char *notes)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
PASS(errctx, harness_start(NULL));
|
|
mock_devices_init();
|
|
PASS(errctx, akbasic_runtime_set_devices(&HARNESS_RUNTIME, NULL, &MOCK_AUDIO, NULL));
|
|
PASS(errctx, akbasic_play_parse(&HARNESS_RUNTIME, notes));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/** @brief The PLAY string parser: notes, octaves, accidentals, rests and durations. */
|
|
static void test_play_parse(void)
|
|
{
|
|
TEST_REQUIRE_OK(parse_notes("O4C"));
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.audio_state.count, 1);
|
|
TEST_REQUIRE(HARNESS_RUNTIME.audio_state.queue[0].hz > 261.5 &&
|
|
HARNESS_RUNTIME.audio_state.queue[0].hz < 261.7,
|
|
"O4C should be middle C, got %f", HARNESS_RUNTIME.audio_state.queue[0].hz);
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.audio_state.queue[0].ms, 500);
|
|
harness_stop();
|
|
|
|
/* A sharp raises by a semitone; C# and D$ are the same pitch. */
|
|
TEST_REQUIRE_OK(parse_notes("O4#CO4$D"));
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.audio_state.count, 2);
|
|
TEST_REQUIRE_FEQ(HARNESS_RUNTIME.audio_state.queue[0].hz,
|
|
HARNESS_RUNTIME.audio_state.queue[1].hz);
|
|
harness_stop();
|
|
|
|
/*
|
|
* B# belongs in the octave above and C$ in the one below. Wrapping the
|
|
* semitone without moving the octave would put B# an octave too low, which
|
|
* is the classic way to get this wrong.
|
|
*/
|
|
TEST_REQUIRE_OK(parse_notes("O4#BO5C"));
|
|
TEST_REQUIRE_FEQ(HARNESS_RUNTIME.audio_state.queue[0].hz,
|
|
HARNESS_RUNTIME.audio_state.queue[1].hz);
|
|
harness_stop();
|
|
|
|
/* Durations persist until changed, and a dot adds half again. */
|
|
TEST_REQUIRE_OK(parse_notes("HCD.E"));
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.audio_state.count, 3);
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.audio_state.queue[0].ms, 1000);
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.audio_state.queue[1].ms, 1000);
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.audio_state.queue[2].ms, 1500);
|
|
harness_stop();
|
|
|
|
/* A rest occupies its time and makes no sound. */
|
|
TEST_REQUIRE_OK(parse_notes("CRC"));
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.audio_state.count, 3);
|
|
TEST_REQUIRE(HARNESS_RUNTIME.audio_state.queue[1].rest, "R should queue a rest");
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.audio_state.queue[1].ms, 500);
|
|
harness_stop();
|
|
|
|
/* V selects the voice for everything after it. */
|
|
TEST_REQUIRE_OK(parse_notes("CV2D"));
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.audio_state.queue[0].voice, 0);
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.audio_state.queue[1].voice, 1);
|
|
harness_stop();
|
|
|
|
TEST_REQUIRE_OK(run_program("10 PLAY \"Z\"\n"));
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "does not understand") != NULL,
|
|
"an unknown PLAY letter should be refused, got \"%s\"", HARNESS_OUTPUT);
|
|
harness_stop();
|
|
|
|
TEST_REQUIRE_OK(run_program("10 PLAY \"V9C\"\n"));
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "PLAY V9") != NULL,
|
|
"voice 9 should be refused, got \"%s\"", HARNESS_OUTPUT);
|
|
harness_stop();
|
|
|
|
/* X selects the filter, which is the one thing here with nothing behind it. */
|
|
TEST_REQUIRE_OK(run_program("10 PLAY \"X1C\"\n"));
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "filter") != NULL,
|
|
"PLAY X should be refused, got \"%s\"", HARNESS_OUTPUT);
|
|
harness_stop();
|
|
}
|
|
|
|
/**
|
|
* @brief PLAY does not block, and the queue drains against the host's clock.
|
|
*
|
|
* This is the assertion that matters most in the file. A C128 holds the program
|
|
* inside PLAY until the last note ends; here PLAY returns immediately and the
|
|
* notes come out of step(). The test owns the clock, so it can assert a note was
|
|
* held for its full length without waiting through it.
|
|
*/
|
|
static void test_play_is_not_blocking(void)
|
|
{
|
|
TEST_REQUIRE_OK(harness_start(NULL));
|
|
mock_devices_init();
|
|
TEST_REQUIRE_OK(akbasic_runtime_set_devices(&HARNESS_RUNTIME, NULL, &MOCK_AUDIO, NULL));
|
|
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 PLAY \"QCD\"\n20 PRINT \"AFTER\"\n"));
|
|
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
|
|
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
|
|
|
|
/*
|
|
* The program ran to completion -- PRINT on line 20 happened -- with a
|
|
* second of music still outstanding. That is the whole point: on a C128 the
|
|
* program would have sat inside PLAY for that second.
|
|
*
|
|
* One note has already gone out. The clock was never set, so it still reads
|
|
* zero, and the step that followed PLAY found the head note's start time
|
|
* (also zero) already reached. Both notes are still counted; only the head
|
|
* has moved.
|
|
*/
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "AFTER\n");
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.audio_state.count, 2);
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.audio_state.head, 1);
|
|
TEST_REQUIRE(strstr(MOCK.log, "tone v0 261") != NULL,
|
|
"the first note should have gone out during the run, got \"%s\"", MOCK.log);
|
|
mock_log_reset();
|
|
|
|
/* Halfway through it, nothing new comes out. */
|
|
TEST_REQUIRE_OK(akbasic_runtime_settime(&HARNESS_RUNTIME, 250));
|
|
TEST_REQUIRE_OK(akbasic_runtime_step(&HARNESS_RUNTIME));
|
|
TEST_REQUIRE_STR(MOCK.log, "");
|
|
|
|
/* At its end, the second note starts. */
|
|
TEST_REQUIRE_OK(akbasic_runtime_settime(&HARNESS_RUNTIME, 500));
|
|
TEST_REQUIRE_OK(akbasic_runtime_step(&HARNESS_RUNTIME));
|
|
TEST_REQUIRE(strstr(MOCK.log, "tone v0 293") != NULL,
|
|
"the second note should have been released, got \"%s\"", MOCK.log);
|
|
|
|
/* Once drained, the queue resets so the next PLAY starts from the front. */
|
|
TEST_REQUIRE_OK(akbasic_runtime_settime(&HARNESS_RUNTIME, 1000));
|
|
TEST_REQUIRE_OK(akbasic_runtime_step(&HARNESS_RUNTIME));
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.audio_state.count, 0);
|
|
harness_stop();
|
|
}
|
|
|
|
/** @brief FILTER is refused rather than ignored, and says why. */
|
|
static void test_filter(void)
|
|
{
|
|
TEST_REQUIRE_OK(run_program("10 FILTER 1000, 1, 0, 0, 5\n"));
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "filter stage") != NULL,
|
|
"FILTER should be refused with a reason, got \"%s\"", HARNESS_OUTPUT);
|
|
harness_stop();
|
|
}
|
|
|
|
/** @brief The verbs that need a device name themselves when there is none. */
|
|
static void test_no_device(void)
|
|
{
|
|
TEST_REQUIRE_OK(harness_start(NULL));
|
|
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 SOUND 1, 1000, 10\n"));
|
|
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
|
|
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "SOUND needs an audio device") != NULL,
|
|
"SOUND without a device should name itself, got \"%s\"", HARNESS_OUTPUT);
|
|
harness_stop();
|
|
|
|
TEST_REQUIRE_OK(harness_start(NULL));
|
|
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 PLAY \"C\"\n"));
|
|
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
|
|
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "PLAY needs an audio device") != NULL,
|
|
"PLAY without a device should name itself, got \"%s\"", HARNESS_OUTPUT);
|
|
harness_stop();
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
test_tables();
|
|
test_sound();
|
|
test_envelope();
|
|
test_vol_and_tempo();
|
|
test_play_parse();
|
|
test_play_is_not_blocking();
|
|
test_filter();
|
|
test_no_device();
|
|
return akbasic_test_failures;
|
|
}
|