Bug: calling set_pou_code with implementationCode only (declarationCode
omitted) would wipe the POU's PROGRAM/VAR...END_VAR block in the binary.
After such a call mirror_export classified the POU as 'UNKNOWN' (no
PROGRAM/FUNCTION_BLOCK keyword in the empty declaration), and the var
block disappeared from the .st mirror file.
Root cause: the TS wrapper substituted '' (empty string) into the Python
template when declarationCode was undefined, giving DECLARATION_CONTENT
= "". The Python script then took the truthy-ish branch (empty string
is not None) and called decl_obj.replace('') -- wiping textual_decl.
Fix: pass explicit SET_DECLARATION / SET_IMPLEMENTATION boolean flags
from the TS wrapper, gate the replace() calls on those flags. Empty
string remains a valid intentional value (caller wants to wipe).
- Reproduced on MCPTest2: PLC_PRG declaration block was wiped between
v1.3.0.0 and v1.3.1.0 by exactly this code path.
- Regression test added in tests/integration/e2e.test.ts covering the
omitted-declarationCode path.
- Existing set_pou_code test extended to assert SET_DECLARATION /
SET_IMPLEMENTATION are emitted in the rendered script.
127 lines
4.8 KiB
TypeScript
127 lines
4.8 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import * as path from 'path';
|
|
import { ScriptManager } from '../../src/script-manager';
|
|
|
|
/**
|
|
* Integration tests that verify the full script preparation pipeline.
|
|
* These don't require CODESYS but verify the template system works end-to-end.
|
|
*/
|
|
describe('E2E Script Preparation', () => {
|
|
const scriptsDir = path.join(__dirname, '..', '..', 'src', 'scripts');
|
|
const mgr = new ScriptManager(scriptsDir);
|
|
|
|
it('open_project script prepares correctly with helpers', () => {
|
|
const script = mgr.prepareScriptWithHelpers(
|
|
'open_project',
|
|
{ PROJECT_FILE_PATH: 'C:\\Projects\\Test.project' },
|
|
['ensure_project_open']
|
|
);
|
|
// Should contain ensure_project_open function
|
|
expect(script).toContain('def ensure_project_open');
|
|
// Should contain the actual open logic
|
|
expect(script).toContain('Project Opened');
|
|
// Path should appear as-is (no escaping) since templates use r"..." raw strings
|
|
expect(script).toContain('C:\\Projects\\Test.project');
|
|
// Should contain success marker
|
|
expect(script).toContain('SCRIPT_SUCCESS');
|
|
});
|
|
|
|
it('create_pou script prepares with both helpers', () => {
|
|
const script = mgr.prepareScriptWithHelpers(
|
|
'create_pou',
|
|
{
|
|
PROJECT_FILE_PATH: 'C:\\test.project',
|
|
POU_NAME: 'MyProgram',
|
|
POU_TYPE_STR: 'Program',
|
|
IMPL_LANGUAGE_STR: 'ST',
|
|
PARENT_PATH: 'Application',
|
|
},
|
|
['ensure_project_open', 'find_object_by_path']
|
|
);
|
|
expect(script).toContain('def ensure_project_open');
|
|
expect(script).toContain('def find_object_by_path_robust');
|
|
expect(script).toContain('MyProgram');
|
|
expect(script).toContain('POU_TYPE_STR = "Program"');
|
|
});
|
|
|
|
it('set_pou_code script handles pre-escaped code content', () => {
|
|
// Simulate what server.ts does: manually escape code for triple-quoted strings
|
|
const declCode = 'VAR\\n x : INT;\\nEND_VAR';
|
|
const implCode = 'x := 42;';
|
|
const sanDecl = declCode.replace(/\\/g, '\\\\').replace(/"""/g, '\\"\\"\\"');
|
|
const sanImpl = implCode.replace(/\\/g, '\\\\').replace(/"""/g, '\\"\\"\\"');
|
|
|
|
const script = mgr.prepareScriptWithHelpers(
|
|
'set_pou_code',
|
|
{
|
|
PROJECT_FILE_PATH: 'C:\\test.project',
|
|
POU_FULL_PATH: 'Application/MyPOU',
|
|
DECLARATION_CONTENT: sanDecl,
|
|
IMPLEMENTATION_CONTENT: sanImpl,
|
|
SET_DECLARATION: 'True',
|
|
SET_IMPLEMENTATION: 'True',
|
|
},
|
|
['ensure_project_open', 'find_object_by_path']
|
|
);
|
|
expect(script).toContain('Application/MyPOU');
|
|
expect(script).toContain('x := 42;');
|
|
expect(script).toContain('SET_DECLARATION = True');
|
|
expect(script).toContain('SET_IMPLEMENTATION = True');
|
|
});
|
|
|
|
it('set_pou_code with omitted declarationCode gates the replace() call', () => {
|
|
// Regression: when caller omits declarationCode, the script must NOT
|
|
// call decl_obj.replace('') -- doing so wipes the POU's
|
|
// PROGRAM/VAR...END_VAR block (binary becomes UNKNOWN POU).
|
|
// server.ts passes SET_DECLARATION='False' in that case.
|
|
const script = mgr.prepareScriptWithHelpers(
|
|
'set_pou_code',
|
|
{
|
|
PROJECT_FILE_PATH: 'C:\\test.project',
|
|
POU_FULL_PATH: 'Application/PLC_PRG',
|
|
DECLARATION_CONTENT: '',
|
|
IMPLEMENTATION_CONTENT: 'x := 1;',
|
|
SET_DECLARATION: 'False',
|
|
SET_IMPLEMENTATION: 'True',
|
|
},
|
|
['ensure_project_open', 'find_object_by_path']
|
|
);
|
|
expect(script).toContain('SET_DECLARATION = False');
|
|
expect(script).toContain('SET_IMPLEMENTATION = True');
|
|
// The skip branch must be reachable
|
|
expect(script).toContain('SET_DECLARATION=False');
|
|
// No leftover {PLACEHOLDER} unsubstituted
|
|
expect(script).not.toMatch(/\{[A-Z_]+\}/);
|
|
});
|
|
|
|
it('check_status script has no placeholders after load', () => {
|
|
const script = mgr.loadTemplate('check_status');
|
|
// check_status has no {PLACEHOLDER} params
|
|
expect(script).not.toMatch(/\{[A-Z_]+\}/);
|
|
expect(script).toContain('SCRIPT_SUCCESS');
|
|
});
|
|
|
|
it('compile_project script prepares with ensure_project_open', () => {
|
|
const script = mgr.prepareScriptWithHelpers(
|
|
'compile_project',
|
|
{ PROJECT_FILE_PATH: 'C:\\test.project' },
|
|
['ensure_project_open']
|
|
);
|
|
expect(script).toContain('def ensure_project_open');
|
|
expect(script).toContain('build()');
|
|
});
|
|
|
|
it('all scripts are loadable', () => {
|
|
const scriptNames = [
|
|
'check_status', 'compile_project', 'create_method', 'create_pou',
|
|
'create_project', 'create_property', 'ensure_project_open',
|
|
'find_object_by_path', 'get_pou_code', 'get_project_structure',
|
|
'open_project', 'save_project', 'set_pou_code', 'watcher',
|
|
];
|
|
for (const name of scriptNames) {
|
|
expect(() => mgr.loadTemplate(name)).not.toThrow();
|
|
const content = mgr.loadTemplate(name);
|
|
expect(content.length).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
});
|