195 lines
7 KiB
TypeScript
195 lines
7 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
|
|
import {
|
|
parseProfileVersion,
|
|
parseProfileLabel,
|
|
parsePinText,
|
|
parseLibraryMdProfile,
|
|
resolveVersionPin,
|
|
decideVersionPin,
|
|
label,
|
|
} from '../../src/version-pin';
|
|
|
|
describe('parseProfileVersion', () => {
|
|
it('maps rawPatch/10 to the install-dir patch convention', () => {
|
|
expect(parseProfileVersion('3.5.19.20')).toEqual({ sp: 19, patch: 2 });
|
|
expect(parseProfileVersion('3.5.21.50')).toEqual({ sp: 21, patch: 5 });
|
|
expect(parseProfileVersion('3.5.22.10')).toEqual({ sp: 22, patch: 1 });
|
|
});
|
|
|
|
it('treats a zero raw patch as patch 0', () => {
|
|
expect(parseProfileVersion('3.5.18.0')).toEqual({ sp: 18, patch: 0 });
|
|
});
|
|
|
|
it('returns undefined for non-version text', () => {
|
|
expect(parseProfileVersion('MSP-Con V3.5 SP19')).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('parseProfileLabel', () => {
|
|
it('parses SP with and without a patch', () => {
|
|
expect(parseProfileLabel('MSP-Con V3.5 SP21 Patch 5')).toEqual({ sp: 21, patch: 5 });
|
|
expect(parseProfileLabel('MSP-Con V3.5 SP19')).toEqual({ sp: 19, patch: 0 });
|
|
});
|
|
|
|
it('returns undefined when there is no SP token', () => {
|
|
expect(parseProfileLabel('3.5.19.20')).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('parsePinText', () => {
|
|
it('accepts either form', () => {
|
|
expect(parsePinText('3.5.19.20')).toEqual({ sp: 19, patch: 2 });
|
|
expect(parsePinText('MSP-Con V3.5 SP19')).toEqual({ sp: 19, patch: 0 });
|
|
});
|
|
|
|
it('prefers the dotted version when a line carries both', () => {
|
|
expect(parsePinText('MSP-Con V3.5 SP21 Patch 5 (3.5.21.50)')).toEqual({ sp: 21, patch: 5 });
|
|
});
|
|
});
|
|
|
|
describe('parseLibraryMdProfile', () => {
|
|
const row =
|
|
'| MSP-Con Development System | `MSP-Con.exe MSP-Con V3.5 SP21 Patch 5, ScriptEngine.plugin 4.2.0.0` |';
|
|
|
|
it('reads the SP/patch out of the generated table row', () => {
|
|
expect(parseLibraryMdProfile(`# Library inventory\n\n${row}\n`)).toEqual({ sp: 21, patch: 5 });
|
|
});
|
|
|
|
it('does not mistake the ScriptEngine version for a profile version', () => {
|
|
// Regression: a naive parseProfileVersion on this row matches "4.2.0.0".
|
|
expect(parseLibraryMdProfile(row)).toEqual({ sp: 21, patch: 5 });
|
|
});
|
|
|
|
it('returns undefined when the row is absent', () => {
|
|
expect(parseLibraryMdProfile('| Project Information.Version | `1.0.0.0` |')).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('resolveVersionPin', () => {
|
|
let dir: string;
|
|
let proj: string;
|
|
|
|
beforeEach(() => {
|
|
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'version-pin-'));
|
|
proj = path.join(dir, 'Thing.project');
|
|
fs.writeFileSync(proj, 'not-a-zip');
|
|
});
|
|
|
|
afterEach(() => {
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('returns undefined when neither source exists', () => {
|
|
expect(resolveVersionPin(proj)).toBeUndefined();
|
|
});
|
|
|
|
it('reads library.md when no pin file is present', () => {
|
|
fs.writeFileSync(
|
|
path.join(dir, 'library.md'),
|
|
'| MSP-Con Development System | `MSP-Con.exe MSP-Con V3.5 SP22 Patch 1` |'
|
|
);
|
|
expect(resolveVersionPin(proj)).toMatchObject({ sp: 22, patch: 1, source: 'library.md' });
|
|
});
|
|
|
|
it('prefers .codesys-version over library.md', () => {
|
|
fs.writeFileSync(
|
|
path.join(dir, 'library.md'),
|
|
'| MSP-Con Development System | `MSP-Con.exe MSP-Con V3.5 SP22 Patch 1` |'
|
|
);
|
|
fs.writeFileSync(path.join(dir, '.codesys-version'), '3.5.19.20\n');
|
|
expect(resolveVersionPin(proj)).toMatchObject({ sp: 19, patch: 2, source: '.codesys-version' });
|
|
});
|
|
|
|
it('skips comments and blank lines in the pin file', () => {
|
|
fs.writeFileSync(
|
|
path.join(dir, '.codesys-version'),
|
|
'# authored on the MarinerX house standard\n\n3.5.19.20\n'
|
|
);
|
|
expect(resolveVersionPin(proj)).toMatchObject({ sp: 19, patch: 2 });
|
|
});
|
|
|
|
it('falls through to library.md when the pin file is unparseable', () => {
|
|
fs.writeFileSync(path.join(dir, '.codesys-version'), 'whatever\n');
|
|
fs.writeFileSync(
|
|
path.join(dir, 'library.md'),
|
|
'| MSP-Con Development System | `MSP-Con.exe MSP-Con V3.5 SP22 Patch 1` |'
|
|
);
|
|
expect(resolveVersionPin(proj)).toMatchObject({ sp: 22, patch: 1, source: 'library.md' });
|
|
});
|
|
});
|
|
|
|
describe('decideVersionPin', () => {
|
|
const projectFilePath = 'C:\\plc\\Thing\\Thing.project';
|
|
const pin = (sp: number, patch: number) =>
|
|
({ sp, patch, source: '.codesys-version' as const, raw: `3.5.${sp}.${patch * 10}` });
|
|
|
|
it('proceeds on an exact match', () => {
|
|
expect(
|
|
decideVersionPin(pin(19, 2), { sp: 19, patch: 2 }, { saves: true, projectFilePath })
|
|
).toEqual({ action: 'proceed' });
|
|
});
|
|
|
|
it('refuses a saving tool when the server is newer', () => {
|
|
const d = decideVersionPin(pin(19, 2), { sp: 21, patch: 5 }, { saves: true, projectFilePath });
|
|
expect(d.action).toBe('refuse');
|
|
expect(d.action === 'refuse' && d.message).toContain('UPGRADE');
|
|
});
|
|
|
|
it('refuses a saving tool when the server is older', () => {
|
|
const d = decideVersionPin(pin(22, 1), { sp: 21, patch: 5 }, { saves: true, projectFilePath });
|
|
expect(d.action).toBe('refuse');
|
|
expect(d.action === 'refuse' && d.message).toContain('DOWNGRADE');
|
|
});
|
|
|
|
it('treats a patch-level difference as a mismatch too', () => {
|
|
// The MarinerX7 failure was an SP difference, but SP21 P5 -> SP21 P2 also
|
|
// rewrites the file; nothing here should special-case same-SP.
|
|
const d = decideVersionPin(pin(21, 2), { sp: 21, patch: 5 }, { saves: true, projectFilePath });
|
|
expect(d.action).toBe('refuse');
|
|
});
|
|
|
|
it('only warns a read-only tool on mismatch', () => {
|
|
const d = decideVersionPin(pin(19, 2), { sp: 21, patch: 5 }, { saves: false, projectFilePath });
|
|
expect(d.action).toBe('proceed-with-warning');
|
|
expect(d.action === 'proceed-with-warning' && d.message).toContain('do NOT run a release');
|
|
});
|
|
|
|
it('refuses a saving tool when no pin is available', () => {
|
|
const d = decideVersionPin(undefined, { sp: 21, patch: 5 }, { saves: true, projectFilePath });
|
|
expect(d.action).toBe('refuse');
|
|
expect(d.action === 'refuse' && d.message).toContain('.codesys-version');
|
|
});
|
|
|
|
it('lets an unpinned read-only tool through, so existing repos keep working', () => {
|
|
expect(
|
|
decideVersionPin(undefined, { sp: 21, patch: 5 }, { saves: false, projectFilePath })
|
|
).toEqual({ action: 'proceed' });
|
|
});
|
|
|
|
it('honours an explicit allowVersionUpgrade override', () => {
|
|
expect(
|
|
decideVersionPin(pin(19, 2), { sp: 21, patch: 5 }, {
|
|
saves: true,
|
|
allowUpgrade: true,
|
|
projectFilePath,
|
|
})
|
|
).toEqual({ action: 'proceed' });
|
|
});
|
|
|
|
it('stays out of the way when the server profile cannot be parsed', () => {
|
|
expect(
|
|
decideVersionPin(pin(19, 2), undefined, { saves: true, projectFilePath })
|
|
).toEqual({ action: 'proceed' });
|
|
});
|
|
});
|
|
|
|
describe('label', () => {
|
|
it('omits "Patch 0"', () => {
|
|
expect(label({ sp: 19, patch: 0 })).toBe('MSP-Con V3.5 SP19');
|
|
expect(label({ sp: 21, patch: 5 })).toBe('MSP-Con V3.5 SP21 Patch 5');
|
|
});
|
|
});
|