DVX_GUI/src/test/basdev/testBasDev.c
2026-10-13 13:49:12 -05:00

1378 lines
56 KiB
C

// The MIT License (MIT)
//
// Copyright (C) 2026 Scott Duensing
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
// testBasDev.c -- the basdev library suite
//
// caseBasicIde runs the DOS fixture PARIDE (src/test/dos/fixtures/paride)
// on the host: a BASIC program that compiles, debugs and builds the
// project in its kid folder through compiler.bas and debug.bas, with its
// DECLARE LIBRARY calls made for real. The other cases drive the shared
// C pieces directly: the build pipeline's compile and source map, the run
// host, the debugger's view of a paused program and the entry points of
// compiler.bas.
#include "dvxtest.h"
#include "../../apps/kpunch/dvxbasic/basBuild.h"
#include "../../apps/kpunch/dvxbasic/basdev/basDebug.h"
#include "../../apps/kpunch/dvxbasic/basdev/basProject.h"
#include "../../apps/kpunch/dvxbasic/basdev/basValidate.h"
#include "../../apps/kpunch/dvxbasic/compiler/lexer.h"
#include "../../apps/kpunch/dvxbasic/formrt/basRunHost.h"
#include "../../apps/kpunch/dvxbasic/formrt/formrt.h"
#include "dvxPlat.h"
#include "dvxRes.h"
#include "dvxTypes.h"
#include "pmModel.h"
#include "../../libs/kpunch/audio/audioDrv.h"
#include "../../libs/kpunch/joystick/joystick.h"
#include "../../libs/kpunch/fileops/fileOps.h"
#include "taskSwch.h"
#include "dvxWm.h"
#include "thirdparty/stb_ds_wrap.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/stat.h>
#define SCREEN_W 800
#define SCREEN_H 600
#define SCREEN_BPP 32
#define BDT_TEXT_MAX 4096 // captured PRINT text / collected rows
#define BDT_TICK_MS 60 // the fake clock per Timer round
#define BDT_MAX_TICKS 200 // rounds before the fixture counts as hung
#define BDT_BP_FILE_LINE 38 // "total = total + i" in kid/kid.bas
#define BDT_PASS_TITLE "PARIDE PASS 109 built"
#define BDT_PARENT_FORM "ParIde"
#define BDT_KID_FORM "KidForm"
#define BDT_KID_TITLE "Kid"
#define BDT_PREFIX "PARIDE "
#define BDT_RENAME_REFS 70000 // references to rename ...
#define BDT_RENAME_NEWLEN 31000 // ... to a name this long: a result past INT32_MAX bytes
#define BDT_RENAME_REF "a.x\n" // one reference
#define BAS_TRUE (-1)
#define KIND_PROGRAM 0 // compiler.bas COMP_KIND_PROGRAM
#define KIND_MODULE 1 // compiler.bas COMP_KIND_MODULE
#define KIND_FORM 2 // compiler.bas COMP_KIND_FORM
#define KIND_CONTROL 4 // compiler.bas COMP_KIND_CONTROL
#define BDT_CTL_KEYS 8 // UserControl header keys (Name, Width ... DefaultEvent)
#define BDT_RUN_ENDED 4 // debug.bas RUN_ENDED
#define BDT_RUN_ERROR 5 // debug.bas RUN_ERROR
#define BDT_DIAL_ICON "BM a dial"
#define KW_CLASS_TYPE 1 // compiler.bas COMP_KW_TYPE
#define KW_CLASS_LITERAL 2 // compiler.bas COMP_KW_LITERAL
#define NO_INDEX (-1)
#define CONST_MISSING (-999999) // includeConst: no such CONST
#define DECIMAL 10
#define HEXADECIMAL 16
#define PERCENT 100 // sound.bas pitches are percent
#define BDT_KW_CONST "CONST "
// A CONST of an include file and the C value it must equal
typedef struct {
const char *file;
const char *name;
int32_t value;
} BdtConstT;
// The compiler.bas / debug.bas entry points the C cases call directly
int32_t CompBuildApp(int32_t mdl, int32_t src, const char *outPath, int32_t release);
const char *CompClassEvents(const char *classText);
int32_t CompCompile(int32_t src, int32_t optionExplicit, int32_t validate);
int32_t CompDesignControls(int32_t mdl, int32_t src, const char *appDir);
int32_t CompDesignRelease(void);
const char *CompError(void);
const char *CompErrorFile(void);
int32_t CompErrorFileLine(void);
const char *CompHelpCtrlTopic(const char *typeName);
const char *CompHelpTopic(const char *word);
int32_t CompInCode(const char *source, int32_t position);
int32_t CompIsIdent(const char *ident);
const char *CompKeyword(int32_t idx);
int32_t CompKeywordClass(int32_t idx);
int32_t CompKeywordCount(void);
int32_t CompKeywordReserved(int32_t idx);
void CompModuleFree(int32_t mdl);
const char *CompProcList(const char *source);
const char *CompProcs(int32_t mdl);
const char *CompRenameInCode(const char *source, const char *oldName, const char *newName, int32_t codeOnly);
int32_t CompSourceAdd(int32_t src, const char *path, const char *text, int32_t kind);
int32_t CompSourceCreate(void);
const char *CompSourceFiles(int32_t src);
void CompSourceFree(int32_t src);
int32_t CompSourceFromProject(int32_t src, const char *dbpPath);
const char *CompTokens(const char *source);
const char *CompWithEvents(const char *source);
const char *DbgExec(int32_t run, const char *line);
int32_t ProjAddFile(int32_t prj, const char *fileName);
void ProjClose(int32_t prj);
const char *ProjCtlHeader(const char *text, const char *key);
const char *ProjCtlKeys(void);
const char *ProjCtlNew(const char *name);
const char *ProjCtlSetHeader(const char *text, const char *key, const char *value);
const char *ProjFile(int32_t prj, int32_t idx);
int32_t ProjFileCount(int32_t prj);
int32_t ProjFileFind(int32_t prj, const char *fileName);
int32_t ProjFileKind(int32_t prj, int32_t idx);
const char *ProjFilePath(int32_t prj, int32_t idx);
const char *ProjGet(int32_t prj, const char *key);
int32_t ProjNew(const char *name, const char *folder);
int32_t ProjOpen(const char *dbpPath);
const char *ProjPath(int32_t prj);
int32_t ProjRemoveFile(int32_t prj, int32_t idx);
int32_t ProjRenameFile(int32_t prj, int32_t idx, const char *newName);
int32_t ProjSave(int32_t prj);
int32_t ProjSet(int32_t prj, const char *key, const char *value);
int32_t RunCreate(int32_t mdl, int32_t src, const char *appDir, const char *args);
void RunFree(int32_t run);
const char *RunOutput(int32_t run);
int32_t RunPoll(int32_t run, int32_t ms);
int32_t RunStart(int32_t run, const char *startupForm, int32_t breakOnStart);
// Text gathered from print callbacks and row callbacks
typedef struct {
char text[BDT_TEXT_MAX];
int32_t len;
} BdtTextT;
// The UserControl of caseDesignControls (a source set's and a project's):
// its header (a built control file's CTL0), then its code
#define BDT_DIAL_HEADER \
"VERSION DVX 1.00\n" \
"Begin UserControl Dial\n" \
" ToolboxIcon = \"dial.bmp\"\n" \
"End\n"
static const char sDialCtl[] =
BDT_DIAL_HEADER
"Private Sub UserControl_Paint()\n"
"End Sub\n";
// The child program of designProject: compiler.bas (the first %s), then
// its own design controls from the project (the second), its module freed
// while they are in use; then it ends.
#define BDT_DESIGN_CHILD_FMT \
"%s\n" \
"Dim s As Long\n" \
"Dim m As Long\n" \
"s = CompSourceCreate()\n" \
"If CompSourceFromProject(s, \"%s\") Then m = CompCompile(s, 0, 0)\n" \
"Print m\n" \
"Print CompDesignControls(m, s, \"\")\n" \
"CompSourceFree s\n" \
"CompModuleFree m\n"
// Prototypes (alphabetical; main last)
static void caseBasicIde(void);
static void caseCDebugger(void);
static void caseCompileApi(void);
static void caseDesignControls(void);
static void caseIncludeConsts(void);
static void caseProjectApi(void);
static void caseProjectControls(void);
static void caseSourceApi(void);
static bool copyKid(const char *kidDir);
static void designProject(const BasFormRtT *rt);
static BasFormT *formOf(const BasFormRtT *rt, const char *formName);
static const char *formTitle(const BasFormRtT *rt, const char *formName);
static int32_t includeConst(const char *text, const char *name);
static bool includeConstIs(const char *text, const char *name, const char *value);
static const char *includeConstValue(const char *text, const char *name);
static int32_t keywordIndex(const char *word);
static BasModuleT *loadProject(const char *dbpPath, BasProjectSourceT *ps, BasValidatorT *validator);
static bool resHas(DvxResHandleT *res, const char *name, const char *want);
static void textPrint(void *ctx, const char *text, bool newline);
static void textRow(void *ctx, const char *const *cells, int32_t cellCount);
int main(void);
// The DOS fixture on the host: the parent runs as a compiled program would
// (its forms loaded, then its Timer events), its library calls are real.
static void caseBasicIde(void) {
tCase("a BASIC program compiles, debugs and builds another (compiler.bas, debug.bas)");
tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
char kidDir[DVX_MAX_PATH];
char carrierDir[DVX_MAX_PATH];
char kidApp[DVX_MAX_PATH];
// The kid project where runDos stages it (TEST\PARIDE\KID beside the
// app), and the STUB carrier CompBuildApp reads (bascomp has one)
snprintf(kidDir, sizeof(kidDir), "%s/paride/kid", tTempDir());
snprintf(kidApp, sizeof(kidApp), "%s/kid.app", tTempDir());
platformPathDirName(BAS_BUILD_STUB_CARRIER, carrierDir, sizeof(carrierDir));
platformMkdirRecursive(carrierDir);
remove(kidApp);
TEST_TRUE(copyKid(kidDir));
TEST_TRUE(platformCopyFile(BDT_BASCOMP, BAS_BUILD_STUB_CARRIER));
BasProjectSourceT ps;
BasModuleT *mod = loadProject(BDT_FIXTURE_DIR "/paride.dbp", &ps, NULL);
TEST_TRUE(mod != NULL);
if (!mod) {
basBuildFreeProject(&ps);
tShutdown();
return;
}
const char *hostError = NULL;
BasRunSpecT spec;
memset(&spec, 0, sizeof(spec));
spec.appDir = tTempDir();
spec.title = BDT_PREFIX;
spec.suppressErrorDialog = true;
BasFormRtT *rt = basRunHostCreate(tCtx(), mod, &spec, &hostError);
TEST_TRUE(rt != NULL);
if (rt) {
for (int32_t i = 0; i < (int32_t)arrlen(ps.frmTexts); i++) {
TEST_TRUE(basRunHostRegisterFrm(rt, ps.frmTexts[i], (int32_t)strlen(ps.frmTexts[i])));
}
// ParIde_Load compiles the kid, makes the run and starts it
basNativeHostSetLive(true);
basRunHostLoadForms(rt, BDT_PARENT_FORM);
basVmSetStepLimit(rt->vm, BAS_VM_DEFAULT_STEP_SLICE);
while (basRunHostSlice(rt) == BAS_VM_STEP_LIMIT) {
}
// The Timer polls the kid through its stops to the end
for (int32_t i = 0; i < BDT_MAX_TICKS && strncmp(formTitle(rt, BDT_PARENT_FORM), BDT_PREFIX, strlen(BDT_PREFIX)) != 0; i++) {
tAdvanceClock(BDT_TICK_MS);
}
TEST_STR_EQ(formTitle(rt, BDT_PARENT_FORM), BDT_PASS_TITLE);
struct stat st;
TEST_TRUE(stat(kidApp, &st) == 0 && st.st_size > 0);
// Unloading frees the kid's run, module and source set
BasFormT *form = formOf(rt, BDT_PARENT_FORM);
if (form) {
basFormRtUnloadForm(rt, form);
}
basNativeHostSetLive(false);
basRunHostDestroy(rt);
}
basModuleFree(mod);
basBuildFreeProject(&ps);
tShutdown();
}
// The shared C pieces the IDE and the library use: a run with a
// breakpoint, the paused program's variables, evaluation, assignment,
// stepping and the [break] captions.
static void caseCDebugger(void) {
tCase("C: run host + debugger view of a paused program");
tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
BasProjectSourceT ps;
BasValidatorT validator;
BasModuleT *mod = loadProject(BDT_FIXTURE_DIR "/kid/kid.dbp", &ps, &validator);
TEST_TRUE(mod != NULL);
TEST_EQ(arrlen(validator.badTypes), 0);
TEST_TRUE(arrlen(validator.entries) > 0);
basValidateFree(&validator);
if (!mod) {
basBuildFreeProject(&ps);
tShutdown();
return;
}
// The breakpoint: kid.bas (the first file) line 38, both ways
int32_t bp = basBuildMapFileLine(ps.map, ps.map[0].path, BDT_BP_FILE_LINE);
const char *mapPath = NULL;
int32_t fileLine = 0;
TEST_TRUE(bp > 0);
TEST_TRUE(basBuildMapLine(ps.map, bp, &mapPath, &fileLine));
TEST_EQ(fileLine, BDT_BP_FILE_LINE);
BdtTextT out;
BdtTextT rows;
const char *hostError = NULL;
BasRunSpecT spec;
memset(&out, 0, sizeof(out));
memset(&spec, 0, sizeof(spec));
spec.appDir = tTempDir();
spec.printFn = textPrint;
spec.printCtx = &out;
spec.suppressErrorDialog = true;
BasFormRtT *rt = basRunHostCreate(tCtx(), mod, &spec, &hostError);
BasVmT *vm = rt ? rt->vm : NULL;
BasBreakTitleT *titles = NULL;
TEST_TRUE(rt != NULL);
if (!rt) {
basModuleFree(mod);
basBuildFreeProject(&ps);
tShutdown();
return;
}
for (int32_t i = 0; i < (int32_t)arrlen(ps.frmTexts); i++) {
basRunHostRegisterFrm(rt, ps.frmTexts[i], (int32_t)strlen(ps.frmTexts[i]));
}
basVmSetBreakpoints(vm, &bp, 1);
basRunHostLoadForms(rt, BDT_KID_FORM);
basVmSetStepLimit(vm, BAS_VM_DEFAULT_STEP_SLICE);
BasVmResultE result;
do {
result = basRunHostSlice(rt);
} while (result == BAS_VM_STEP_LIMIT);
TEST_EQ(result, BAS_VM_BREAKPOINT);
TEST_EQ(basVmGetCurrentLine(vm), bp);
TEST_STR_EQ(basDbgProcNameForPc(vm, vm->pc), "AddUp");
TEST_TRUE(basDbgCurProcIdx(vm) >= 0);
// Locals: the parameter, the local and the shared module variables
memset(&rows, 0, sizeof(rows));
TEST_TRUE(basDbgLocals(vm, textRow, &rows) >= 4);
TEST_TRUE(strstr(rows.text, "n|Long|4\n") != NULL);
TEST_TRUE(strstr(rows.text, "i|Long|1\n") != NULL);
TEST_TRUE(strstr(rows.text, "total|Long|0\n") != NULL);
TEST_TRUE(strstr(rows.text, "label|String|\"sum\"\n") != NULL);
// Call stack: AddUp at the line, then the module frame
char expect[BDT_TEXT_MAX];
memset(&rows, 0, sizeof(rows));
TEST_EQ(basDbgCallStack(vm, bp, textRow, &rows), 2);
snprintf(expect, sizeof(expect), "AddUp|%d\n" BAS_DBG_MODULE_FRAME "|\n", (int)bp);
TEST_STR_EQ(rows.text, expect);
// Watch: a variable path is read directly, an expression is evaluated
char value[BAS_DBG_VALUE_LEN];
TEST_TRUE(basDbgEval(vm, "n", value, sizeof(value)));
TEST_STR_EQ(value, "4");
TEST_TRUE(basDbgEval(vm, "total + n * 10", value, sizeof(value)));
TEST_EQ(atoi(value), 40);
TEST_TRUE(!basDbgEval(vm, "nosuch(", value, sizeof(value)));
TEST_TRUE(!basDbgEval(NULL, "n", value, sizeof(value)));
// [break] on the program's form captions while paused
basRunHostTagBreak(rt, &titles, true);
TEST_STR_EQ(formTitle(rt, BDT_KID_FORM), BDT_KID_TITLE BAS_RUN_BREAK_TAG);
basRunHostTagBreak(rt, &titles, false);
TEST_STR_EQ(formTitle(rt, BDT_KID_FORM), BDT_KID_TITLE);
arrfree(titles);
// Immediate: assignment, statement, expression, error; and SetVar
memset(&out, 0, sizeof(out));
TEST_TRUE(basDbgExec(vm, "total = 3", textPrint, &out));
TEST_STR_EQ(out.text, "total = 3\n");
memset(&out, 0, sizeof(out));
TEST_TRUE(!basDbgExec(vm, "PRINT total * 2", textPrint, &out));
TEST_EQ(atoi(out.text), 6);
memset(&out, 0, sizeof(out));
TEST_TRUE(!basDbgExec(NULL, "6 * 7", textPrint, &out));
TEST_EQ(atoi(out.text), 42);
memset(&out, 0, sizeof(out));
basDbgExec(vm, "label = ", textPrint, &out);
TEST_TRUE(strncmp(out.text, "Error: ", strlen("Error: ")) == 0);
memset(&out, 0, sizeof(out));
TEST_TRUE(basDbgSetVar(vm, "total", "total + 4", textPrint, &out));
TEST_STR_EQ(out.text, "total = 7\n");
memset(&out, 0, sizeof(out));
TEST_TRUE(!basDbgSetVar(vm, "nosuch", "1", textPrint, &out));
TEST_TRUE(!basDbgSetVar(vm, "label", "1 +", textPrint, &out));
// Step over: the breakpoint line ran once (7 + 1)
basVmStepOver(vm);
basVmResume(vm);
do {
result = basRunHostSlice(rt);
} while (result == BAS_VM_STEP_LIMIT);
TEST_EQ(result, BAS_VM_BREAKPOINT);
TEST_TRUE(basDbgEval(vm, "total", value, sizeof(value)));
TEST_STR_EQ(value, "8");
// Run free to the end: 8 + 2 + 3 + 4
basVmSetBreakpoints(vm, NULL, 0);
basVmClearStepState(vm);
basVmResume(vm);
memset(&out, 0, sizeof(out));
do {
result = basRunHostSlice(rt);
} while (result == BAS_VM_STEP_LIMIT);
TEST_EQ(result, BAS_VM_HALTED);
TEST_TRUE(strstr(out.text, "17") != NULL);
TEST_TRUE(!basRunHostAlive(rt));
basRunHostDestroy(rt);
basModuleFree(mod);
basBuildFreeProject(&ps);
tShutdown();
}
// compiler.bas from C: source sets, error mapping, procedure lists,
// identifiers, keywords, the validator's unknown types and member typos.
static void caseCompileApi(void) {
tCase("compiler.bas entry points: errors, maps, procs, validation");
tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
// A compile error maps back to its file and line
int32_t src = CompSourceCreate();
TEST_TRUE(src > 0);
TEST_EQ(CompSourceAdd(src, "first.bas", "DIM a AS LONG\na = 1\n", KIND_MODULE), BAS_TRUE);
TEST_EQ(CompSourceAdd(src, "second.bas", "DIM b AS LONG\nb = \n", KIND_MODULE), BAS_TRUE);
TEST_EQ(CompCompile(src, 0, 0), 0);
TEST_TRUE(CompError()[0] != '\0');
TEST_TRUE(strncmp(CompError(), "Line", strlen("Line")) != 0);
TEST_STR_EQ(CompErrorFile(), "second.bas");
TEST_EQ(CompErrorFileLine(), 2);
TEST_TRUE(strstr(CompSourceFiles(src), "first.bas\t1\t") != NULL);
CompSourceFree(src);
// A form whose control type no widget library registers
src = CompSourceCreate();
TEST_EQ(CompSourceAdd(src, "f.frm", "Begin Form F\n Begin Bogus b1\n End\nEnd\n", -1), BAS_TRUE);
TEST_EQ(CompCompile(src, 0, BAS_TRUE), 0);
TEST_TRUE(strstr(CompError(), "type 'Bogus' is not registered") != NULL);
CompSourceFree(src);
// A member typo on a known control fails at compile time when
// validating, and compiles without validation
const char *frm = "Begin Form F\n Begin Label lbl\n End\nEnd\n\nSUB F_Load\n lbl.Captoin = \"x\"\nEND SUB\n";
src = CompSourceCreate();
TEST_EQ(CompSourceAdd(src, "f.frm", frm, KIND_FORM), BAS_TRUE);
TEST_EQ(CompCompile(src, 0, BAS_TRUE), 0);
TEST_STR_EQ(CompErrorFile(), "f.frm");
TEST_EQ(CompErrorFileLine(), 7);
int32_t mdl = CompCompile(src, 0, 0);
TEST_TRUE(mdl > 0);
TEST_STR_EQ(CompProcs(mdl), "F_Load\tSub\tF\n");
CompModuleFree(mdl);
CompSourceFree(src);
// Procedure blocks of source text, identifiers, keywords
TEST_STR_EQ(CompProcList("x = 1\nSUB A\nEND SUB\nPrivate Function B(n)\n B = n\nEnd Function\n"), "Sub\tA\t2\t3\nFunction\tB\t4\t6\n");
TEST_EQ(CompIsIdent("abc_1"), BAS_TRUE);
TEST_EQ(CompIsIdent("1abc"), 0);
TEST_TRUE(CompKeywordCount() > 0);
TEST_TRUE(CompKeyword(0)[0] != '\0');
TEST_STR_EQ(CompKeyword(-1), "");
// The Immediate window without a program
TEST_EQ(atoi(DbgExec(0, "6 * 7")), 42);
tShutdown();
}
// compiler.bas for a form designer: a source set's UserControls made
// control types of the calling program alone (CompDesignControls), their
// module kept past CompModuleFree until they go (CompDesignRelease), a
// second call replacing the first, another program's module refused; a
// control project's (designProject); the header keys (ProjCtlKeys$).
static void caseDesignControls(void) {
tCase("compiler.bas designer: CompDesignControls, CompDesignRelease, CompBuildApp of a control project, ProjCtlKeys$");
tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
static const char host[] = "Dim x As Integer\n";
TEST_STR_EQ(ProjCtlKeys(), "Name\tName\nWidth\tInteger\nHeight\tInteger\nCanGetFocus\tBoolean\nBackColor\tColor\nToolboxIcon\tText\nDescription\tText\nDefaultEvent\tText\n");
char dir[DVX_MAX_PATH];
char want[DVX_MAX_PATH];
char path[DVX_MAX_PATH];
snprintf(dir, sizeof(dir), "%s/dsgnctl", tTempDir());
snprintf(want, sizeof(want), "%s/dial.bmp", dir);
int32_t src = CompSourceCreate();
TEST_EQ(CompSourceAdd(src, "dial.ctl", sDialCtl, KIND_CONTROL), BAS_TRUE);
int32_t mdl = CompCompile(src, 0, 0);
TEST_TRUE(mdl > 0);
// Outside a program there is no designer to register for
TEST_EQ(CompDesignControls(mdl, src, dir), 0);
TEST_TRUE(CompError()[0] != '\0');
TEST_EQ(CompDesignRelease(), BAS_TRUE);
// The calling program: a runtime made current
BasCompileErrorT err;
BasModuleT *hostMod = basBuildCompile(host, (int32_t)strlen(host), false, NULL, NULL, &err);
const char *hostError = NULL;
BasRunSpecT spec;
memset(&spec, 0, sizeof(spec));
spec.appDir = tTempDir();
spec.title = BDT_PARENT_FORM;
spec.suppressErrorDialog = true;
BasFormRtT *rt = hostMod ? basRunHostCreate(tCtx(), hostMod, &spec, &hostError) : NULL;
BasFormRtT *prev = basFormRtSetCurrent(rt);
TEST_TRUE(rt != NULL);
TEST_EQ(CompDesignControls(mdl, src, dir), 1);
TEST_STR_EQ(CompError(), "");
const WgtIfaceT *iface = basUcOwnIface(rt, "Dial");
TEST_TRUE(iface != NULL);
TEST_PTR_EQ(basUcOwnIface(NULL, "Dial"), NULL);
TEST_TRUE(basUcIconPath(iface, path, sizeof(path)));
TEST_STR_EQ(path, want);
// Freed while its controls are in use: kept, and no new use of it
CompModuleFree(mdl);
TEST_TRUE(basUcOwnIface(rt, "Dial") != NULL);
TEST_EQ(CompDesignControls(mdl, src, dir), 0);
TEST_TRUE(basUcOwnIface(rt, "Dial") != NULL);
TEST_EQ(CompDesignRelease(), BAS_TRUE);
TEST_PTR_EQ(basUcOwnIface(rt, "Dial"), NULL);
TEST_EQ(CompDesignControls(mdl, src, dir), 0);
// Another module's controls, App.Path the caller's, then replaced
int32_t next = CompCompile(src, 0, 0);
snprintf(want, sizeof(want), "%s/dial.bmp", tTempDir());
TEST_EQ(CompDesignControls(next, src, ""), 1);
TEST_TRUE(basUcIconPath(basUcOwnIface(rt, "Dial"), path, sizeof(path)));
TEST_STR_EQ(path, want);
TEST_EQ(CompDesignControls(next, src, dir), 1);
TEST_TRUE(basUcOwnIface(rt, "Dial") != NULL);
TEST_EQ(CompDesignRelease(), BAS_TRUE);
CompModuleFree(next);
// Another program's module is refused: it would go when that one ends
int32_t *ownApp = dvxMemAppIdPtr;
int32_t otherApp = (ownApp ? *ownApp : 0) + 1;
dvxMemAppIdPtr = &otherApp;
int32_t foreign = CompCompile(src, 0, 0);
dvxMemAppIdPtr = ownApp;
TEST_TRUE(foreign > 0);
TEST_EQ(CompDesignControls(foreign, src, dir), 0);
TEST_PTR_EQ(basUcOwnIface(rt, "Dial"), NULL);
CompModuleFree(foreign);
CompSourceFree(src);
if (rt) {
designProject(rt);
}
basFormRtSetCurrent(prev);
if (rt) {
basRunHostDestroy(rt);
}
basModuleFree(hostMod);
tShutdown();
}
// The CONSTs of the include files that mirror C values: one source of
// truth, checked here (a C value changed without its CONST fails).
static void caseIncludeConsts(void) {
static const BdtConstT consts[] = {
{ "resource.bas", "RES_NAME_MAX", DVX_RES_NAME_MAX },
{ "system.bas", "MOUSE_WHEEL_STEP_MIN", MOUSE_WHEEL_STEP_MIN },
{ "system.bas", "MOUSE_WHEEL_STEP_MAX", MOUSE_WHEEL_STEP_MAX },
{ "system.bas", "MOUSE_WHEEL_STEP_DEFAULT", MOUSE_WHEEL_STEP_DEFAULT },
{ "system.bas", "MOUSE_DBLCLICK_DEFAULT_MS", MOUSE_DBLCLICK_DEFAULT_MS },
{ "system.bas", "MOUSE_SPEED_DEFAULT", MOUSE_SPEED_DEFAULT },
{ "system.bas", "SYS_VIDEO_DEFAULT_WIDTH", DVX_DEFAULT_VIDEO_W },
{ "system.bas", "SYS_VIDEO_DEFAULT_HEIGHT", DVX_DEFAULT_VIDEO_H },
{ "system.bas", "SYS_VIDEO_DEFAULT_BPP", DVX_DEFAULT_VIDEO_BPP },
{ "shell.bas", "SHELL_VIEW_LARGE", PmViewLargeE },
{ "shell.bas", "SHELL_VIEW_SMALL", PmViewSmallE },
{ "shell.bas", "SHELL_VIEW_LIST", PmViewListE },
{ "shell.bas", "SHELL_PRIORITY_LOW", TS_PRIORITY_LOW },
{ "shell.bas", "SHELL_PRIORITY_NORMAL", TS_PRIORITY_NORMAL },
{ "shell.bas", "SHELL_PRIORITY_HIGH", TS_PRIORITY_HIGH },
{ "fileops.bas", "FILEOPS_ATTR_READONLY", FILEOPS_ATTR_READONLY },
{ "fileops.bas", "FILEOPS_ATTR_HIDDEN", FILEOPS_ATTR_HIDDEN },
{ "fileops.bas", "FILEOPS_ATTR_SYSTEM", FILEOPS_ATTR_SYSTEM },
{ "fileops.bas", "FILEOPS_ATTR_ARCHIVE", FILEOPS_ATTR_ARCHIVE },
{ "fileops.bas", "FILEOPS_ATTR_EDITABLE", FILEOPS_ATTR_EDITABLE },
{ "fileops.bas", "FILEOPS_DRIVE_FIXED", PlatformDriveFixedE },
{ "fileops.bas", "FILEOPS_DRIVE_REMOVABLE", PlatformDriveRemovableE },
{ "fileops.bas", "FILEOPS_DRIVE_REMOTE", PlatformDriveRemoteE },
{ "fileops.bas", "FILEOPS_SORT_NAME", FileOpsSortNameE },
{ "fileops.bas", "FILEOPS_SORT_SIZE", FileOpsSortSizeE },
{ "fileops.bas", "FILEOPS_SORT_TYPE", FileOpsSortTypeE },
{ "fileops.bas", "FILEOPS_SORT_DATE", FileOpsSortDateE },
{ "fileops.bas", "FILEOPS_COPY", FileOpsCopyE },
{ "fileops.bas", "FILEOPS_MOVE", FileOpsMoveE },
{ "fileops.bas", "FILEOPS_DELETE", FileOpsDeleteE },
{ "fileops.bas", "FILEOPS_STEP_BUSY", FileOpsStepBusyE },
{ "fileops.bas", "FILEOPS_STEP_ASK", FileOpsStepAskE },
{ "fileops.bas", "FILEOPS_STEP_ERROR", FileOpsStepErrorE },
{ "fileops.bas", "FILEOPS_STEP_DONE", FileOpsStepDoneE },
{ "fileops.bas", "FILEOPS_STEP_CANCELLED", FileOpsStepCancelledE },
{ "fileops.bas", "FILEOPS_ANSWER_YES", FileOpsAnswerYesE },
{ "fileops.bas", "FILEOPS_ANSWER_NO", FileOpsAnswerNoE },
{ "fileops.bas", "FILEOPS_ANSWER_ALL", FileOpsAnswerAllE },
{ "fileops.bas", "FILEOPS_ANSWER_CANCEL", FileOpsAnswerCancelE },
{ "fileops.bas", "FILEOPS_ANSWER_RETRY", FileOpsAnswerRetryE },
{ "fileops.bas", "FILEOPS_ANSWER_SKIP", FileOpsAnswerSkipE },
{ "compiler.bas", "COMP_KW_OTHER", BAS_KW_CLASS_OTHER },
{ "compiler.bas", "COMP_KW_TYPE", BAS_KW_CLASS_TYPE },
{ "compiler.bas", "COMP_KW_LITERAL", BAS_KW_CLASS_LITERAL },
{ "compiler.bas", "COMP_KIND_MODULE", BAS_BUILD_WRAP_MODULE },
{ "compiler.bas", "COMP_KIND_FORM", BAS_BUILD_WRAP_FORM },
{ "compiler.bas", "COMP_KIND_CLASS", BAS_BUILD_WRAP_CLASS },
{ "compiler.bas", "COMP_KIND_CONTROL", BAS_BUILD_WRAP_CONTROL },
{ "sound.bas", "SOUND_EVENT_DEFAULT", SoundDefaultE },
{ "sound.bas", "SOUND_EVENT_START", SoundStartE },
{ "sound.bas", "SOUND_EVENT_EXIT", SoundExitE },
{ "sound.bas", "SOUND_EVENT_ERROR", SoundErrorE },
{ "sound.bas", "SOUND_EVENT_WARNING", SoundWarningE },
{ "sound.bas", "SOUND_EVENT_QUESTION", SoundQuestionE },
{ "sound.bas", "SOUND_EVENT_INFO", SoundInfoE },
{ "sound.bas", "SOUND_EVENT_WINDOW_OPEN", SoundWindowOpenE },
{ "sound.bas", "SOUND_EVENT_WINDOW_CLOSE", SoundWindowCloseE },
{ "sound.bas", "SOUND_PITCH_NORMAL", AUDIO_PITCH_NORMAL * PERCENT / AUDIO_PITCH_NORMAL },
{ "sound.bas", "SOUND_PITCH_MIN", AUDIO_PITCH_MIN * PERCENT / AUDIO_PITCH_NORMAL },
{ "sound.bas", "SOUND_PITCH_MAX", AUDIO_PITCH_MAX * PERCENT / AUDIO_PITCH_NORMAL },
{ "sound.bas", "SOUND_RATE_MIN", AUDIO_RATE_MIN },
{ "sound.bas", "SOUND_RATE_MAX", AUDIO_RATE_MAX },
{ "sound.bas", "SOUND_RATE_DEFAULT", AUDIO_RATE_DEFAULT },
{ "sound.bas", "SOUND_STREAM_MS_MIN", AUDIO_STREAM_MS_MIN },
{ "sound.bas", "SOUND_STREAM_MS_DEFAULT", AUDIO_STREAM_MS_DEFAULT },
{ "sound.bas", "SOUND_STREAM_MS_MAX", AUDIO_STREAM_MS_MAX },
{ "sound.bas", "FM_VOICES_MAX", AUDIO_FM_VOICES_MAX },
{ "sound.bas", "FM_CHIP_HZ", AUDIO_FM_CHIP_HZ },
{ "sound.bas", "FM_FNUM_MAX", AUDIO_FM_FNUM_MAX },
{ "sound.bas", "FM_BLOCK_MAX", AUDIO_FM_BLOCK_MAX },
{ "sound.bas", "FM_FNUM_HI_SHIFT", AUDIO_FM_FNUM_HI_SHIFT },
{ "sound.bas", "FM_BLOCK_SHIFT", AUDIO_FM_BLOCK_SHIFT },
{ "sound.bas", "FM_MOD_CHAR", AUDIO_FM_MOD_CHAR },
{ "sound.bas", "FM_CAR_CHAR", AUDIO_FM_CAR_CHAR },
{ "sound.bas", "FM_MOD_LEVEL", AUDIO_FM_MOD_LEVEL },
{ "sound.bas", "FM_CAR_LEVEL", AUDIO_FM_CAR_LEVEL },
{ "sound.bas", "FM_MOD_ATTACK", AUDIO_FM_MOD_ATTACK },
{ "sound.bas", "FM_CAR_ATTACK", AUDIO_FM_CAR_ATTACK },
{ "sound.bas", "FM_MOD_SUSTAIN", AUDIO_FM_MOD_SUSTAIN },
{ "sound.bas", "FM_CAR_SUSTAIN", AUDIO_FM_CAR_SUSTAIN },
{ "sound.bas", "FM_FNUM_LOW", AUDIO_FM_FNUM_LOW },
{ "sound.bas", "FM_KEY_BLOCK", AUDIO_FM_KEY_BLOCK },
{ "sound.bas", "FM_FEEDBACK", AUDIO_FM_FEEDBACK },
{ "sound.bas", "FM_MOD_WAVE", AUDIO_FM_MOD_WAVE },
{ "sound.bas", "FM_CAR_WAVE", AUDIO_FM_CAR_WAVE },
{ "sound.bas", "FM_KEY_ON", AUDIO_FM_KEY_ON },
{ "sound.bas", "FM_OUT_LEFT", AUDIO_FM_OUT_LEFT },
{ "sound.bas", "FM_OUT_RIGHT", AUDIO_FM_OUT_RIGHT },
{ "sound.bas", "FM_OUT_BOTH", AUDIO_FM_OUT_BOTH },
{ "sound.bas", "FM_ADDITIVE", AUDIO_FM_ADDITIVE },
{ "sound.bas", "FM_BANK_REGS", AUDIO_FM_BANK_REGS },
{ "joystick.bas", "JOY_CAL_MIN", JOY_CAL_MIN },
{ "joystick.bas", "JOY_CAL_CENTER", JOY_CAL_CENTER },
{ "joystick.bas", "JOY_CAL_MAX", JOY_CAL_MAX },
{ "joystick.bas", "JOY_DEADZONE_DEFAULT", JOY_DEADZONE_DEFAULT },
{ "joystick.bas", "JOY_DEADZONE_MAX", JOY_DEADZONE_MAX },
{ "joystick.bas", "JOY_POLL_HZ_DEFAULT", JOY_POLL_HZ_DEFAULT },
{ "joystick.bas", "JOY_POLL_HZ_MIN", JOY_POLL_HZ_MIN },
{ "joystick.bas", "JOY_POLL_HZ_MAX", JOY_POLL_HZ_MAX },
{ "joystick.bas", "JOY_HAT_CENTERED", JOY_HAT_CENTERED },
{ "joystick.bas", "JOY_AXIS_MIN", JOY_AXIS_MIN },
{ "joystick.bas", "JOY_AXIS_MAX", JOY_AXIS_MAX },
{ "joystick.bas", "JOY_CHOICE_GAMEPORT", JoyChoiceGamePortE },
{ "joystick.bas", "JOY_CHOICE_FALLBACK", JoyChoiceFallbackE }
};
tCase("include-file CONSTs equal their C values");
for (int32_t i = 0; i < DVX_ARRAY_LEN(consts); i++) {
char path[DVX_MAX_PATH];
int32_t len = 0;
snprintf(path, sizeof(path), BDT_INCLUDE_DIR "/%s", consts[i].file);
char *text = platformReadFile(path, &len);
TEST_TRUE(text != NULL);
if (text) {
TEST_EQ(includeConst(text, consts[i].name), consts[i].value);
free(text);
}
}
// String CONSTs
int32_t resLen = 0;
char *resBas = platformReadFile(BDT_INCLUDE_DIR "/resource.bas", &resLen);
TEST_TRUE(resBas != NULL);
if (resBas) {
TEST_TRUE(includeConstIs(resBas, "RES_NAME_ICON", DVX_RES_NAME_ICON));
TEST_TRUE(includeConstIs(resBas, "RES_NAME_WIDGET_ICON", DVX_RES_NAME_WIDGET_ICON));
free(resBas);
}
}
// The .dbp model from BASIC: a new project with files and settings,
// saved, opened again, a file renamed and removed.
static void caseProjectApi(void) {
tCase("compiler.bas projects: ProjNew, ProjSave, ProjOpen, files, settings");
tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
char dir[DVX_MAX_PATH];
char dbp[DVX_MAX_PATH];
snprintf(dir, sizeof(dir), "%s/projapi", tTempDir());
snprintf(dbp, sizeof(dbp), "%s/demo.dbp", dir);
platformMkdirRecursive(dir);
remove(dbp);
int32_t prj = ProjNew("demo", dir);
TEST_TRUE(prj > 0);
TEST_EQ(ProjAddFile(prj, "main.bas"), 0);
TEST_EQ(ProjAddFile(prj, "form1.frm"), 1);
TEST_EQ(ProjAddFile(prj, "MAIN.BAS"), NO_INDEX);
TEST_EQ(ProjFileKind(prj, 1), KIND_FORM);
TEST_EQ(ProjFileKind(prj, 0), KIND_MODULE);
TEST_EQ(ProjFileKind(prj, 2), NO_INDEX);
TEST_EQ(ProjSet(prj, "Author", "Somebody"), BAS_TRUE);
TEST_EQ(ProjSet(prj, "optionexplicit", " -1"), BAS_TRUE);
TEST_EQ(ProjSet(prj, "StartupForm", "Form1"), BAS_TRUE);
TEST_EQ(ProjSet(prj, "Bogus", "x"), 0);
TEST_EQ(ProjSave(prj), BAS_TRUE);
ProjClose(prj);
TEST_EQ(ProjOpen("no-such-project.dbp"), 0);
prj = ProjOpen(dbp);
TEST_TRUE(prj > 0);
TEST_STR_EQ(ProjGet(prj, "author"), "Somebody");
TEST_STR_EQ(ProjGet(prj, "Name"), "demo");
TEST_STR_EQ(ProjGet(prj, "OptionExplicit"), "true");
TEST_STR_EQ(ProjGet(prj, "MultiInstance"), "false");
TEST_STR_EQ(ProjGet(prj, "StartupForm"), "Form1");
TEST_STR_EQ(ProjGet(prj, "Bogus"), "");
TEST_STR_EQ(ProjPath(prj), dbp);
TEST_EQ(ProjFileCount(prj), 2);
TEST_STR_EQ(ProjFile(prj, 0), "main.bas");
TEST_EQ(ProjFileFind(prj, "FORM1.FRM"), 1);
TEST_TRUE(strstr(ProjFilePath(prj, 1), "projapi") != NULL);
// A rename keeps the kind's extension and saves the project at once
TEST_EQ(ProjRenameFile(prj, 0, "x.frm"), 0);
TEST_TRUE(CompError()[0] != '\0');
TEST_EQ(ProjRenameFile(prj, 0, "app"), BAS_TRUE);
TEST_STR_EQ(ProjFile(prj, 0), "app.bas");
TEST_EQ(ProjRemoveFile(prj, 1), BAS_TRUE);
TEST_EQ(ProjRemoveFile(prj, 1), 0);
TEST_EQ(ProjFileCount(prj), 1);
ProjClose(prj);
prj = ProjOpen(dbp);
TEST_EQ(ProjFileFind(prj, "app.bas"), 0);
ProjClose(prj);
TEST_EQ(ProjFileCount(prj), 0);
tShutdown();
}
// A control project and a UserControl's header: [Project] Type = Control
// and the [Controls] section written, read and kept by renames; the
// header keys read with their defaults and set key by key, the code after
// the header untouched (compiler.bas and the C model).
static void caseProjectControls(void) {
tCase("compiler.bas projects: control projects, [Controls], UserControl headers");
tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
static const char ctl[] =
"VERSION DVX 1.00\n"
"Begin UserControl Gauge\n"
" Width = 120\n"
" CanGetFocus = False\n"
"End\n"
"Option Explicit\n"
"Private Sub UserControl_Paint()\n"
"End Sub\n";
static const char ctlCode[] = "Option Explicit\nPrivate Sub UserControl_Paint()\nEnd Sub\n";
// The header keys and their defaults
TEST_STR_EQ(ProjCtlHeader(ctl, "name"), "Gauge");
TEST_STR_EQ(ProjCtlHeader(ctl, "width"), "120");
TEST_STR_EQ(ProjCtlHeader(ctl, "Height"), "64");
TEST_STR_EQ(ProjCtlHeader(ctl, "CanGetFocus"), "False");
TEST_STR_EQ(ProjCtlHeader(ctl, "BackColor"), "");
TEST_STR_EQ(ProjCtlHeader(ctl, "Description"), "");
TEST_STR_EQ(ProjCtlHeader(ctl, "Bogus"), "");
TEST_STR_EQ(ProjCtlHeader("x = 1\n", "Width"), "");
TEST_TRUE(prjCtlKeyAt(0) != NULL);
TEST_STR_EQ(prjCtlKeyAt(0)->key, BAS_PROP_NAME);
TEST_STR_EQ(prjCtlKeyAt(1)->key, BAS_UC_KEY_WIDTH);
TEST_STR_EQ(prjCtlKeyAt(BDT_CTL_KEYS - 1)->key, BAS_UC_KEY_DEFAULTEVENT);
TEST_TRUE(prjCtlKeyAt(BDT_CTL_KEYS) == NULL);
TEST_TRUE(prjCtlKeyAt(-1) == NULL);
TEST_EQ(prjCtlKeyFind("backcolor")->kind, PrjCtlColorE);
TEST_EQ(prjCtlKeyFind("CANGETFOCUS")->kind, PrjCtlBoolE);
TEST_EQ(prjCtlKeyFind("ToolboxIcon")->kind, PrjCtlTextE);
TEST_EQ(prjCtlKeyFind("NAME")->kind, PrjCtlNameE);
TEST_TRUE(prjCtlKeyFind("Caption") == NULL);
// A key's line changed in place, added before End, removed
TEST_STR_EQ(ProjCtlSetHeader(ctl, "WIDTH", " 200 "), "VERSION DVX 1.00\nBegin UserControl Gauge\n Width = 200\n CanGetFocus = False\nEnd\nOption Explicit\nPrivate Sub UserControl_Paint()\nEnd Sub\n");
TEST_STR_EQ(CompError(), "");
TEST_STR_EQ(ProjCtlSetHeader(ctl, "CanGetFocus", "-1"), "VERSION DVX 1.00\nBegin UserControl Gauge\n Width = 120\n CanGetFocus = True\nEnd\nOption Explicit\nPrivate Sub UserControl_Paint()\nEnd Sub\n");
TEST_STR_EQ(ProjCtlSetHeader(ctl, "BackColor", "&hff0000"), "VERSION DVX 1.00\nBegin UserControl Gauge\n Width = 120\n CanGetFocus = False\n BackColor = &H00FF0000&\nEnd\nOption Explicit\nPrivate Sub UserControl_Paint()\nEnd Sub\n");
TEST_STR_EQ(ProjCtlSetHeader(ctl, "Description", "A gauge"), "VERSION DVX 1.00\nBegin UserControl Gauge\n Width = 120\n CanGetFocus = False\n Description = \"A gauge\"\nEnd\nOption Explicit\nPrivate Sub UserControl_Paint()\nEnd Sub\n");
TEST_STR_EQ(ProjCtlSetHeader(ctl, "CanGetFocus", ""), "VERSION DVX 1.00\nBegin UserControl Gauge\n Width = 120\nEnd\nOption Explicit\nPrivate Sub UserControl_Paint()\nEnd Sub\n");
TEST_STR_EQ(ProjCtlSetHeader(ctl, "Height", ""), ctl);
TEST_STR_EQ(ProjCtlSetHeader(ctl, "Name", "Meter"), "VERSION DVX 1.00\nBegin UserControl Meter\n Width = 120\n CanGetFocus = False\nEnd\nOption Explicit\nPrivate Sub UserControl_Paint()\nEnd Sub\n");
// Read back as set (a decimal color too), the code kept
char colored[sizeof(ctl) * 2];
snprintf(colored, sizeof(colored), "%s", ProjCtlSetHeader(ctl, "BackColor", "255"));
TEST_STR_EQ(ProjCtlHeader(colored, "BackColor"), "&H000000FF&");
TEST_TRUE(strstr(colored, ctlCode) != NULL);
TEST_STR_EQ(ProjCtlHeader(ProjCtlSetHeader(ctl, "DefaultEvent", "Changed"), "DefaultEvent"), "Changed");
// Values a key does not take
TEST_STR_EQ(ProjCtlSetHeader(ctl, "Width", "wide"), "");
TEST_TRUE(CompError()[0] != '\0');
TEST_STR_EQ(ProjCtlSetHeader(ctl, "Width", "-5"), "");
TEST_STR_EQ(ProjCtlSetHeader(ctl, "BackColor", "&HXYZ"), "");
TEST_STR_EQ(ProjCtlSetHeader(ctl, "Description", "say \"hi\""), "");
TEST_STR_EQ(ProjCtlSetHeader(ctl, "Description", "two\nlines"), "");
TEST_STR_EQ(ProjCtlSetHeader(ctl, "Bogus", "1"), "");
TEST_STR_EQ(ProjCtlSetHeader(ctl, "Name", ""), "");
TEST_STR_EQ(ProjCtlSetHeader(ctl, "Name", "2Bad"), "");
TEST_STR_EQ(ProjCtlSetHeader(ctl, "Name", "Two Words"), "");
TEST_STR_EQ(ProjCtlSetHeader(ctlCode, "Width", "1"), "");
// A new control's text, and it compiles
static const char fresh[] = "VERSION DVX 1.00\nBegin UserControl Dial\n Width = 64\n Height = 64\nEnd\n\nOption Explicit\n\nPrivate Sub UserControl_Paint ()\n\nEnd Sub\n";
TEST_STR_EQ(ProjCtlNew("Dial"), fresh);
TEST_STR_EQ(CompError(), "");
TEST_STR_EQ(ProjCtlNew("9Dial"), "");
TEST_TRUE(CompError()[0] != '\0');
TEST_STR_EQ(ProjCtlNew(""), "");
int32_t src = CompSourceCreate();
TEST_EQ(CompSourceAdd(src, "dial.ctl", fresh, KIND_CONTROL), BAS_TRUE);
int32_t mdl = CompCompile(src, BAS_TRUE, 0);
TEST_TRUE(mdl > 0);
CompModuleFree(mdl);
CompSourceFree(src);
// A control project: Type and [Controls] saved and read back
char dir[DVX_MAX_PATH];
char dbp[DVX_MAX_PATH];
snprintf(dir, sizeof(dir), "%s/projctl", tTempDir());
snprintf(dbp, sizeof(dbp), "%s/gauge.dbp", dir);
platformMkdirRecursive(dir);
remove(dbp);
int32_t prj = ProjNew("gauge", dir);
TEST_STR_EQ(ProjGet(prj, "Type"), "");
TEST_EQ(ProjSet(prj, "type", BAS_PROJECT_TYPE_CONTROL), BAS_TRUE);
TEST_EQ(ProjAddFile(prj, "gauge.ctl"), 0);
TEST_EQ(ProjAddFile(prj, "util.bas"), 1);
TEST_EQ(ProjFileKind(prj, 0), KIND_CONTROL);
TEST_EQ(ProjSave(prj), BAS_TRUE);
ProjClose(prj);
int32_t len = 0;
char *text = platformReadFile(dbp, &len);
TEST_TRUE(text != NULL);
TEST_TRUE(text && strstr(text, "[" BAS_INI_SECTION_CONTROLS "]") != NULL);
TEST_TRUE(text && strstr(text, "gauge.ctl") != NULL);
TEST_TRUE(text && strstr(text, BAS_INI_KEY_TYPE) != NULL);
free(text);
PrjStateT model;
prjInit(&model);
TEST_TRUE(prjLoad(&model, dbp));
TEST_TRUE(prjIsControlProject(&model));
TEST_EQ(prjFileCount(&model), 2);
TEST_EQ(model.files[prjFileIndexByPath(&model, "gauge.ctl")].kind, PrjKindControlE);
TEST_EQ(model.files[0].kind, PrjKindModuleE);
TEST_EQ(prjKindForPath("X.CTL"), PrjKindControlE);
TEST_EQ(prjKindWrap(PrjKindControlE), BAS_BUILD_WRAP_CONTROL);
TEST_EQ(prjFieldSet(&model, BAS_INI_KEY_TYPE, ""), true);
TEST_TRUE(!prjIsControlProject(&model));
prjClose(&model);
// Modules load before controls (the compile order)
prj = ProjOpen(dbp);
TEST_STR_EQ(ProjGet(prj, "Type"), BAS_PROJECT_TYPE_CONTROL);
TEST_EQ(ProjFileFind(prj, "GAUGE.CTL"), 1);
TEST_EQ(ProjFileKind(prj, 1), KIND_CONTROL);
TEST_EQ(ProjRenameFile(prj, 1, "meter.bas"), 0);
TEST_EQ(ProjRenameFile(prj, 1, "meter"), BAS_TRUE);
TEST_STR_EQ(ProjFile(prj, 1), "meter.ctl");
ProjClose(prj);
prj = ProjOpen(dbp);
TEST_EQ(ProjFileKind(prj, ProjFileFind(prj, "meter.ctl")), KIND_CONTROL);
TEST_EQ(ProjRemoveFile(prj, 1), BAS_TRUE);
TEST_EQ(ProjSave(prj), BAS_TRUE);
ProjClose(prj);
prj = ProjOpen(dbp);
TEST_EQ(ProjFileCount(prj), 1);
TEST_EQ(ProjFileKind(prj, 0), KIND_MODULE);
ProjClose(prj);
// The validator: a project's own control type is known, its members
// are left to the run time (a registered one may be an older build)
BasValidatorT v;
memset(&v, 0, sizeof(v));
basValidateAddType(&v, "Gauge");
TEST_TRUE(basValidateIsPropValid(&v, "gauge", "Needle"));
TEST_TRUE(basValidateIsMethodValid(&v, "Gauge", "Spin"));
TEST_TRUE(!basValidateIsPropValid(&v, "CommandButton", "Needle"));
TEST_TRUE(!basValidateIsMethodValid(NULL, "CommandButton", "Spin"));
basValidateFree(&v);
tShutdown();
}
// Source text as the compiler reads it: tokens, code positions, renaming
// in code, WithEvents variables, class Events, help topics, keyword
// classes.
static void caseSourceApi(void) {
tCase("compiler.bas source text: tokens, in-code, rename, events, help topics, keyword flags");
TEST_STR_EQ(CompTokens("x = \"a\tb\" ' note\nIF y THEN\n"), "1\t1\t1\tIdentifier\tx\n1\t3\t1\tSymbol\t=\n1\t5\t5\tString\t\"a b\"\n1\t11\t6\tComment\t' note\n2\t1\t2\tKeyword\tIF\n2\t4\t1\tIdentifier\ty\n2\t6\t4\tKeyword\tTHEN\n");
TEST_STR_EQ(CompTokens("REM all of it\n"), "1\t1\t13\tComment\tREM all of it\n");
TEST_STR_EQ(CompTokens("s = \"open\n"), "1\t1\t1\tIdentifier\ts\n1\t3\t1\tSymbol\t=\n1\t5\t5\tError\t\"open\n");
const char *code = "a = \"Cmd1.x\" ' Cmd1.y\nCmd1.z = 1\ns = \"abc\n";
TEST_EQ(CompInCode(code, 1), BAS_TRUE);
TEST_EQ(CompInCode(code, 4), BAS_TRUE);
TEST_EQ(CompInCode(code, 6), 0);
TEST_EQ(CompInCode(code, 16), 0);
TEST_EQ(CompInCode(code, 23), BAS_TRUE);
TEST_EQ(CompInCode(code, 39), 0);
TEST_EQ(CompInCode(code, 0), 0);
const char *frm = "Command1.Caption = \"Command1.x\"\nSub Command1_Click\n' Command1.y\nMyCommand1.z = 1\n";
TEST_STR_EQ(CompRenameInCode(frm, "command1", "Go", BAS_TRUE), "Go.Caption = \"Command1.x\"\nSub Go_Click\n' Command1.y\nMyCommand1.z = 1\n");
TEST_STR_EQ(CompRenameInCode(frm, "Command1", "Go", 0), "Go.Caption = \"Go.x\"\nSub Go_Click\n' Go.y\nMyCommand1.z = 1\n");
TEST_STR_EQ(CompRenameInCode(frm, "Label1", "Go", BAS_TRUE), frm);
// A result too long for an int32_t length leaves the text unchanged
// (its size overflowed int32_t before)
size_t refLen = strlen(BDT_RENAME_REF);
char *many = (char *)malloc(BDT_RENAME_REFS * refLen + 1);
char *newName = (char *)malloc(BDT_RENAME_NEWLEN + 1);
if (many && newName) {
for (int32_t i = 0; i < BDT_RENAME_REFS; i++) {
memcpy(many + (size_t)i * refLen, BDT_RENAME_REF, refLen);
}
many[BDT_RENAME_REFS * refLen] = '\0';
memset(newName, 'b', BDT_RENAME_NEWLEN);
newName[BDT_RENAME_NEWLEN] = '\0';
TEST_STR_EQ(CompRenameInCode(many, "a", newName, 0), many);
}
free(many);
free(newName);
TEST_STR_EQ(CompWithEvents("Dim WithEvents a As Foo, b As Long\nPrivate WithEvents c As Bar\nx = 1 ' Dim WithEvents d As Baz\n"), "a\tFoo\nc\tBar\n");
TEST_STR_EQ(CompClassEvents("Public Event Changed(Value As Integer)\nEvent Done\nx = \"Event Nope\"\n"), "Changed\tValue As Integer\nDone\t\n");
TEST_STR_EQ(CompHelpTopic("withevents"), "lang.events");
TEST_STR_EQ(CompHelpTopic("NoSuchWord"), "");
TEST_STR_EQ(CompHelpCtrlTopic("CommandButton"), "ctrl.commandbutton");
TEST_EQ(CompKeywordClass(keywordIndex("INTEGER")), KW_CLASS_TYPE);
TEST_EQ(CompKeywordReserved(keywordIndex("INTEGER")), BAS_TRUE);
TEST_EQ(CompKeywordClass(keywordIndex("BYTE")), KW_CLASS_TYPE);
TEST_EQ(CompKeywordReserved(keywordIndex("BYTE")), 0);
TEST_EQ(CompKeywordClass(keywordIndex("TRUE")), KW_CLASS_LITERAL);
TEST_EQ(CompKeywordClass(NO_INDEX), NO_INDEX);
TEST_EQ(CompKeywordReserved(CompKeywordCount()), 0);
}
// Copy the fixture's kid project into kidDir.
static bool copyKid(const char *kidDir) {
static const char *const files[] = { "kid.dbp", "kid.bas", "kidform.frm" };
bool ok = platformMkdirRecursive(kidDir) == 0;
for (int32_t i = 0; ok && i < DVX_ARRAY_LEN(files); i++) {
char from[DVX_MAX_PATH];
char to[DVX_MAX_PATH];
snprintf(from, sizeof(from), BDT_FIXTURE_DIR "/kid/%s", files[i]);
snprintf(to, sizeof(to), "%s/%s", kidDir, files[i]);
ok = platformCopyFile(from, to);
}
return ok;
}
// caseDesignControls with program rt current: a control project's set
// (App.Path its folder; built into a control file by CompBuildApp) and a
// child program's design controls, which go when it ends along with its
// hold on their module, so that handle is free again.
static void designProject(const BasFormRtT *rt) {
char dir[DVX_MAX_PATH];
char dbp[DVX_MAX_PATH];
char ctlPath[DVX_MAX_PATH];
char iconPath[DVX_MAX_PATH];
char wgt[DVX_MAX_PATH];
char path[DVX_MAX_PATH];
char ctl0[BAS_RES_FORM_NAME_LEN];
snprintf(dir, sizeof(dir), "%s/dsgnprj", tTempDir());
snprintf(dbp, sizeof(dbp), "%s/dial.dbp", dir);
snprintf(ctlPath, sizeof(ctlPath), "%s/dial.ctl", dir);
snprintf(iconPath, sizeof(iconPath), "%s/dial.bmp", dir);
snprintf(wgt, sizeof(wgt), "%s/dial.wgt", tTempDir());
snprintf(ctl0, sizeof(ctl0), BAS_RES_CTL_FMT, (long)0);
platformMkdirRecursive(dir);
remove(dbp);
remove(wgt);
TEST_TRUE(tWriteFile(ctlPath, sDialCtl, (int32_t)strlen(sDialCtl)));
TEST_TRUE(tWriteFile(iconPath, BDT_DIAL_ICON, (int32_t)sizeof(BDT_DIAL_ICON)));
int32_t prj = ProjNew("dial", dir);
TEST_EQ(ProjSet(prj, "Type", BAS_PROJECT_TYPE_CONTROL), BAS_TRUE);
TEST_EQ(ProjAddFile(prj, "dial.ctl"), 0);
TEST_EQ(ProjSave(prj), BAS_TRUE);
ProjClose(prj);
// A project's set: App.Path (and the ToolboxIcon) its folder
int32_t src = CompSourceCreate();
TEST_EQ(CompSourceFromProject(src, dbp), BAS_TRUE);
int32_t mdl = CompCompile(src, 0, 0);
TEST_EQ(CompDesignControls(mdl, src, ""), 1);
TEST_TRUE(basUcIconPath(basUcOwnIface(rt, "Dial"), path, sizeof(path)));
TEST_STR_EQ(path, iconPath);
TEST_EQ(CompDesignRelease(), BAS_TRUE);
// A control project builds a control file: the module, the header
// text, the toolbox picture and name (no Description: the name)
TEST_EQ(CompBuildApp(mdl, src, wgt, 0), BAS_TRUE);
CompModuleFree(mdl);
CompSourceFree(src);
DvxResHandleT *res = dvxResOpen(wgt);
TEST_TRUE(res != NULL);
TEST_TRUE(resHas(res, BAS_RES_MODULE, NULL));
TEST_TRUE(resHas(res, ctl0, BDT_DIAL_HEADER));
TEST_TRUE(resHas(res, BAS_CTL_RES_ICON, BDT_DIAL_ICON));
TEST_TRUE(resHas(res, BAS_CTL_RES_NAME, "Dial"));
if (res) {
dvxResClose(res);
}
// A child program's own design controls
int32_t len = 0;
char *inc = platformReadFile(BDT_INCLUDE_DIR "/compiler.bas", &len);
size_t size = (size_t)len + sizeof(BDT_DESIGN_CHILD_FMT) + strlen(dbp);
char *text = inc ? (char *)malloc(size) : NULL;
int32_t kidSrc = CompSourceCreate();
TEST_TRUE(text != NULL);
if (text) {
snprintf(text, size, BDT_DESIGN_CHILD_FMT, inc, dbp);
TEST_EQ(CompSourceAdd(kidSrc, "child.bas", text, KIND_PROGRAM), BAS_TRUE);
}
int32_t kidMdl = CompCompile(kidSrc, 0, 0);
int32_t run = RunCreate(kidMdl, kidSrc, "", "");
int32_t state = BDT_RUN_ERROR;
basNativeHostSetLive(true);
TEST_EQ(RunStart(run, "", 0), BAS_TRUE);
for (int32_t i = 0; i < BDT_MAX_TICKS && (state = RunPoll(run, BDT_TICK_MS)) != BDT_RUN_ENDED && state != BDT_RUN_ERROR; i++) {
}
basNativeHostSetLive(false);
TEST_EQ(state, BDT_RUN_ENDED);
char *end = NULL;
int32_t freed = (int32_t)strtol(RunOutput(run), &end, DECIMAL);
int32_t designed = (int32_t)strtol(end, NULL, DECIMAL);
TEST_EQ(designed, 1);
// Its module went with it: handles are given out lowest free first,
// so at most "freed" new ones reach its number
int32_t *probes = NULL;
bool reused = false;
for (int32_t i = 0; i < freed && !reused; i++) {
int32_t h = CompSourceCreate();
arrput(probes, h);
reused = (h == freed);
}
TEST_TRUE(reused);
for (int32_t i = 0; i < (int32_t)arrlen(probes); i++) {
CompSourceFree(probes[i]);
}
arrfree(probes);
RunFree(run);
CompModuleFree(kidMdl);
CompSourceFree(kidSrc);
free(text);
free(inc);
}
static BasFormT *formOf(const BasFormRtT *rt, const char *formName) {
for (int32_t i = 0; i < (int32_t)arrlen(rt->forms); i++) {
if (strcasecmp(rt->forms[i]->name, formName) == 0) {
return rt->forms[i];
}
}
return NULL;
}
// The caption of a program's form window ("" when it has none).
static const char *formTitle(const BasFormRtT *rt, const char *formName) {
const BasFormT *form = formOf(rt, formName);
return (form && form->window) ? form->window->title : "";
}
// The value of "CONST name = value" in BASIC source text, CONST_MISSING
// when it has none.
static int32_t includeConst(const char *text, const char *name) {
const char *p = includeConstValue(text, name);
if (!p) {
return CONST_MISSING;
}
// &H20 is hexadecimal, as in BASIC
if (p[0] == '&' && (p[1] == 'H' || p[1] == 'h')) {
return (int32_t)strtol(p + 2, NULL, HEXADECIMAL);
}
return (int32_t)strtol(p, NULL, DECIMAL);
}
// Whether "CONST name = "value"" in BASIC source text is that string.
static bool includeConstIs(const char *text, const char *name, const char *value) {
const char *p = includeConstValue(text, name);
size_t len = strlen(value);
return p && p[0] == '"' && strncmp(p + 1, value, len) == 0 && p[len + 1] == '"';
}
// Where the value of "CONST name = value" starts in BASIC source text,
// NULL when it has none.
static const char *includeConstValue(const char *text, const char *name) {
size_t nameLen = strlen(name);
for (const char *line = text; *line; ) {
const char *p = dvxSkipWs(line);
const char *end = strchr(line, '\n');
if (strncasecmp(p, BDT_KW_CONST, strlen(BDT_KW_CONST)) == 0) {
p = dvxSkipWs(p + strlen(BDT_KW_CONST));
if (strncasecmp(p, name, nameLen) == 0 && !basIsIdentChar(p[nameLen])) {
p = dvxSkipWs(p + nameLen);
if (*p == '=') {
return dvxSkipWs(p + 1);
}
}
}
line = end ? end + 1 : line + strlen(line);
}
return NULL;
}
// The index of keyword word in CompKeyword$'s list, -1 when it is not one.
static int32_t keywordIndex(const char *word) {
for (int32_t i = 0; i < CompKeywordCount(); i++) {
if (strcmp(CompKeyword(i), word) == 0) {
return i;
}
}
return NO_INDEX;
}
// Read and compile a project as bascomp does (validator: also scan its
// forms and check Control.Member names). NULL when it does not compile.
static BasModuleT *loadProject(const char *dbpPath, BasProjectSourceT *ps, BasValidatorT *validator) {
PrefsHandleT *prefs = NULL;
char dir[DVX_MAX_PATH];
BasCompileErrorT err;
BasCtrlValidatorT cb;
memset(ps, 0, sizeof(*ps));
if (validator) {
memset(validator, 0, sizeof(*validator));
basValidateCallbacks(validator, &cb);
}
if (!basBuildOpenProject(dbpPath, &prefs, dir, sizeof(dir))) {
return NULL;
}
bool ok = basBuildConcatProject(prefs, dir, ps);
BasModuleT *mod = NULL;
prefsClose(prefs);
if (!ok) {
return NULL;
}
for (int32_t i = 0; validator && i < (int32_t)arrlen(ps->frmTexts); i++) {
basValidateScanFrm(validator, ps->frmTexts[i], (int32_t)strlen(ps->frmTexts[i]), NULL, NULL);
}
mod = basBuildCompile(ps->source, ps->sourceLen, ps->optionExplicit, validator ? &cb : NULL, validator ? &validator->compiler : NULL, &err);
if (!mod) {
printf(" compile: %s\n", err.message);
}
return mod;
}
// True when res has resource name and, unless want is NULL, it is the
// text want (with its NUL, as the build writes text).
static bool resHas(DvxResHandleT *res, const char *name, const char *want) {
uint32_t size = 0;
char *data = res ? (char *)dvxResRead(res, name, &size) : NULL;
bool same = data && (!want || (size == strlen(want) + 1 && memcmp(data, want, size) == 0));
free(data);
return same;
}
static void textPrint(void *ctx, const char *text, bool newline) {
BdtTextT *t = (BdtTextT *)ctx;
if (text) {
t->len = basDbgAppendDropping(t->text, BDT_TEXT_MAX, t->len, text, (int32_t)strlen(text));
}
if (newline) {
t->len = basDbgAppendDropping(t->text, BDT_TEXT_MAX, t->len, "\n", 1);
}
}
// A debugger row as "cell|cell|cell\n".
static void textRow(void *ctx, const char *const *cells, int32_t cellCount) {
BdtTextT *t = (BdtTextT *)ctx;
for (int32_t i = 0; i < cellCount; i++) {
t->len = basDbgAppendDropping(t->text, BDT_TEXT_MAX, t->len, i > 0 ? "|" : "", i > 0 ? 1 : 0);
t->len = basDbgAppendDropping(t->text, BDT_TEXT_MAX, t->len, cells[i], (int32_t)strlen(cells[i]));
}
t->len = basDbgAppendDropping(t->text, BDT_TEXT_MAX, t->len, "\n", 1);
}
int main(void) {
setvbuf(stdout, NULL, _IOLBF, 0);
tSuiteBegin("basdev");
tProcessInit(DVXTEST_WIDGET_DIR);
caseCompileApi();
caseDesignControls();
caseIncludeConsts();
caseSourceApi();
caseProjectApi();
caseProjectControls();
caseCDebugger();
caseBasicIde();
return tSuiteEnd();
}