// 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 #include #include #include #include #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(); }