mirror of
https://github.com/whyour/qinglong.git
synced 2026-10-16 22:35:32 +08:00
Merge Python dependency and Shell regression fixes
fix: Python dependency status, GNU awk compatibility and dev regressions
This commit is contained in:
+38
-11
@@ -716,17 +716,20 @@ function isPyprojectToml(name: string): boolean {
|
||||
return name.endsWith('pyproject.toml');
|
||||
}
|
||||
|
||||
export function isPythonSourceDependency(name: string): boolean {
|
||||
const source = name.trim();
|
||||
return (
|
||||
isGitHubUrl(source) || isRequirementsFile(source) || isPyprojectToml(source)
|
||||
);
|
||||
}
|
||||
|
||||
export function getGetCommand(type: DependenceTypes, name: string): string {
|
||||
const trimmedName = name.trim();
|
||||
|
||||
// For Python dependencies installed from GitHub or requirements files,
|
||||
// we can't reliably check if they're installed, so skip the check
|
||||
if (type === DependenceTypes.python3) {
|
||||
if (
|
||||
isGitHubUrl(trimmedName) ||
|
||||
isRequirementsFile(trimmedName) ||
|
||||
isPyprojectToml(trimmedName)
|
||||
) {
|
||||
if (isPythonSourceDependency(trimmedName)) {
|
||||
// Return a command that will always indicate not installed
|
||||
// This ensures GitHub URLs and requirements files are always installed
|
||||
return 'echo ""';
|
||||
@@ -736,10 +739,30 @@ export function getGetCommand(type: DependenceTypes, name: string): string {
|
||||
const baseCommands = {
|
||||
[DependenceTypes.nodejs]: `pnpm ls -g | grep "${trimmedName}" | head -1`,
|
||||
[DependenceTypes.python3]: `${getPythonExecutable()} -c ${shellQuote(
|
||||
`from importlib.metadata import version\nimport importlib.util as u\nname=${JSON.stringify(
|
||||
trimmedName,
|
||||
)}\ntry:\n print(version(name))\nexcept Exception:\n print(name if u.find_spec(name) else '')`,
|
||||
)}`,
|
||||
`from importlib.metadata import version, PackageNotFoundError
|
||||
import importlib.util
|
||||
import sys
|
||||
try:
|
||||
try:
|
||||
from packaging.requirements import Requirement
|
||||
except ImportError:
|
||||
from pip._vendor.packaging.requirements import Requirement
|
||||
requirement = Requirement(sys.argv[1])
|
||||
if requirement.marker and not requirement.marker.evaluate():
|
||||
print('not applicable')
|
||||
else:
|
||||
try:
|
||||
installed = version(requirement.name)
|
||||
except PackageNotFoundError:
|
||||
if not requirement.specifier and not requirement.extras and not requirement.url:
|
||||
if importlib.util.find_spec(requirement.name):
|
||||
print(requirement.name)
|
||||
else:
|
||||
if requirement.specifier.contains(installed, prereleases=True):
|
||||
print(installed)
|
||||
except Exception:
|
||||
pass`,
|
||||
)} ${shellQuote(trimmedName)}`,
|
||||
[DependenceTypes.linux]:
|
||||
getOsTypeSync() === 'Alpine'
|
||||
? `apk info -es ${trimmedName}`
|
||||
@@ -824,9 +847,13 @@ export function getUninstallCommand(
|
||||
'Remove installed Python distributions separately by package name.',
|
||||
)}`;
|
||||
}
|
||||
const packageName =
|
||||
name.match(
|
||||
/^([A-Za-z0-9][A-Za-z0-9._-]*)(?:\[[^\]]+\])?(?=\s*(?:[<>=!~;@]|$))/,
|
||||
)?.[1] || name;
|
||||
return `${getPythonPip()} uninstall --disable-pip-version-check${
|
||||
PYTHON_VENV_DIR ? '' : ' --root-user-action=ignore'
|
||||
} -y -- ${shellQuote(name)}`;
|
||||
} -y -- ${shellQuote(packageName)}`;
|
||||
}
|
||||
const baseCommands = {
|
||||
[DependenceTypes.nodejs]: 'pnpm remove -g',
|
||||
@@ -917,7 +944,7 @@ async function _updateLinuxMirror(
|
||||
throw new Error('Unsupported OS type for updating mirrors.');
|
||||
const target = mirror || defaults[osType];
|
||||
normalizeMirror(target);
|
||||
const S = isInContainer() ? 'sudo ' : '';
|
||||
const S = osType !== 'Alpine' && isInContainer() ? 'sudo ' : '';
|
||||
const update =
|
||||
osType === 'Alpine' ? 'apk update' : `${S}apt-get update --error-on=any`;
|
||||
let file =
|
||||
|
||||
+35
-12
@@ -20,6 +20,7 @@ import {
|
||||
getInstallCommand,
|
||||
getUninstallCommand,
|
||||
getGetCommand,
|
||||
isPythonSourceDependency,
|
||||
} from '../config/util';
|
||||
import dayjs from 'dayjs';
|
||||
import taskLimit from '../shared/pLimit';
|
||||
@@ -76,21 +77,25 @@ export default class DependenceService {
|
||||
for (const doc of docs) {
|
||||
taskLimit.removeQueuedDependency(doc);
|
||||
}
|
||||
const unInstalledDeps = docs.filter(
|
||||
(x) => x.status !== DependenceStatus.installed,
|
||||
);
|
||||
const installedDeps = docs.filter(
|
||||
(x) => x.status === DependenceStatus.installed,
|
||||
(x) =>
|
||||
x.status === DependenceStatus.installed ||
|
||||
// Older list refreshes marked unprobeable sources as failed. They may
|
||||
// still have installed distributions (including partial pip installs).
|
||||
(x.status === DependenceStatus.installFailed &&
|
||||
x.type === DependenceTypes.python3 &&
|
||||
isPythonSourceDependency(x.name)),
|
||||
);
|
||||
const unInstalledDeps = docs.filter((x) => !installedDeps.includes(x));
|
||||
await this.removeDb(unInstalledDeps.map((x) => x.id!));
|
||||
|
||||
if (installedDeps.length) {
|
||||
await DependenceModel.update(
|
||||
{ status: DependenceStatus.queued, log: [] },
|
||||
{ where: { id: ids } },
|
||||
{ where: { id: installedDeps.map((x) => x.id!) } },
|
||||
);
|
||||
|
||||
this.installDependenceOneByOne(docs, false, force);
|
||||
this.installDependenceOneByOne(installedDeps, false, force);
|
||||
}
|
||||
return docs;
|
||||
}
|
||||
@@ -156,9 +161,15 @@ export default class DependenceService {
|
||||
},
|
||||
});
|
||||
const checks = await concurrentRun(
|
||||
docs.map((doc) => async () => {
|
||||
return (await this.isDependenceInstalled(doc)) ? undefined : doc.id;
|
||||
}),
|
||||
docs
|
||||
.filter(
|
||||
(doc) =>
|
||||
doc.type !== DependenceTypes.python3 ||
|
||||
!isPythonSourceDependency(doc.name),
|
||||
)
|
||||
.map((doc) => async () => {
|
||||
return (await this.isDependenceInstalled(doc)) ? undefined : doc.id;
|
||||
}),
|
||||
5,
|
||||
);
|
||||
const missingIds = (checks || []).filter(
|
||||
@@ -177,7 +188,10 @@ export default class DependenceService {
|
||||
let depName = dependency.name.trim();
|
||||
const depVersionStr = versionDependenceCommandTypes[dependency.type];
|
||||
let depVersion = '';
|
||||
if (depName.includes(depVersionStr)) {
|
||||
if (
|
||||
dependency.type !== DependenceTypes.python3 &&
|
||||
depName.includes(depVersionStr)
|
||||
) {
|
||||
const symbolRegx = new RegExp(
|
||||
`(.*)${depVersionStr}([0-9\\.\\-\\+a-zA-Z]*)`,
|
||||
);
|
||||
@@ -305,6 +319,9 @@ export default class DependenceService {
|
||||
: getUninstallCommand(dependency.type, dependency.name.trim());
|
||||
} catch (error) {
|
||||
taskLimit.removeQueuedDependency(dependency);
|
||||
if (!isInstall && force) {
|
||||
return this.removeDb([dependency.id!]).then(() => null);
|
||||
}
|
||||
return DependenceModel.update(
|
||||
{
|
||||
status: isInstall
|
||||
@@ -387,7 +404,13 @@ export default class DependenceService {
|
||||
this.updateLog(depIds, message);
|
||||
|
||||
// 判断是否已经安装过依赖
|
||||
if (isInstall && !force) {
|
||||
// pip must resolve optional dependencies even if the base distribution
|
||||
// is already present; its version alone cannot prove extras are ready.
|
||||
if (
|
||||
isInstall &&
|
||||
!force &&
|
||||
!(isPythonDependence && depName.includes('['))
|
||||
) {
|
||||
let getCommand = getGetCommand(dependency.type, depName);
|
||||
const depVersionStr = versionDependenceCommandTypes[dependency.type];
|
||||
if (isLinuxDependence) {
|
||||
@@ -395,7 +418,7 @@ export default class DependenceService {
|
||||
}
|
||||
|
||||
let depVersion = '';
|
||||
if (depName.includes(depVersionStr)) {
|
||||
if (!isPythonDependence && depName.includes(depVersionStr)) {
|
||||
const symbolRegx = new RegExp(
|
||||
`(.*)${depVersionStr}([0-9\\.\\-\\+a-zA-Z]*)`,
|
||||
);
|
||||
|
||||
+1
-1
@@ -44,7 +44,7 @@ gen_array_scripts() {
|
||||
if [[ -f $file ]] && [[ $file == *.js && $file != sendNotify.js ]]; then
|
||||
let i++
|
||||
array_scripts[i]=$file
|
||||
array_scripts_name[i]=$(grep -- "new Env" "$file" | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:.*\('\''\|"\)\([^"'\'']*\)\('\''\|"\).*:\2:' | sed 's:"::g' | sed "s:'::g" | head -1)
|
||||
array_scripts_name[i]=$(grep -- "new Env" "$file" | awk -F '[(]' '{print $2}' | awk -F '[)]' '{print $1}' | sed 's:.*\('\''\|"\)\([^"'\'']*\)\('\''\|"\).*:\2:' | sed 's:"::g' | sed "s:'::g" | head -1)
|
||||
[[ -z ${array_scripts_name[i]} ]] && array_scripts_name[i]="<未识别出活动名称>"
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -700,6 +700,8 @@
|
||||
"已保存,但旧历史清理失败,请检查存储权限和空间": "Saved, but old history could not be removed. Check storage permissions and free space.",
|
||||
"保存失败": "Unable to save",
|
||||
"通知日志": "Notification log",
|
||||
"标题": "Title",
|
||||
"内容": "Content",
|
||||
"发送时间": "Sent at",
|
||||
"推送渠道": "Channel",
|
||||
"发送状态": "Delivery status",
|
||||
|
||||
@@ -700,6 +700,8 @@
|
||||
"已保存,但旧历史清理失败,请检查存储权限和空间": "已保存,但旧历史清理失败,请检查存储权限和空间",
|
||||
"保存失败": "保存失败",
|
||||
"通知日志": "通知日志",
|
||||
"标题": "标题",
|
||||
"内容": "内容",
|
||||
"发送时间": "发送时间",
|
||||
"推送渠道": "推送渠道",
|
||||
"发送状态": "发送状态",
|
||||
|
||||
@@ -61,6 +61,7 @@ test('dependency listing marks cache entries missing from disk for reinstall', a
|
||||
fileExist: async () => false,
|
||||
getGetCommand: (_type, name) => `check:${name}`,
|
||||
getInstallCommand: () => '',
|
||||
isPythonSourceDependency: () => false,
|
||||
getPid: async () => 0,
|
||||
getUninstallCommand: () => '',
|
||||
killTask: async () => {},
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
const load = require('../helpers/load-security-module.cjs');
|
||||
|
||||
const types = { nodejs: 0, python3: 1, linux: 2 };
|
||||
const statuses = { installed: 1, installFailed: 2, queued: 6, cancelled: 7 };
|
||||
const util = load('back/config/util.ts', {
|
||||
'./index': {},
|
||||
'./const': {},
|
||||
'../loaders/logger': {},
|
||||
'../shared/utils': {},
|
||||
'../data/dependence': { DependenceTypes: types },
|
||||
});
|
||||
|
||||
function serviceFor(docs) {
|
||||
const probes = [],
|
||||
removed = [],
|
||||
uninstalls = [];
|
||||
const Service = load('back/services/dependence.ts', {
|
||||
'../config': {},
|
||||
'../data/dependence': {
|
||||
Dependence: class {},
|
||||
DependenceTypes: types,
|
||||
DependenceStatus: statuses,
|
||||
versionDependenceCommandTypes: { 0: '@', 1: '==', 2: '=' },
|
||||
DependenceModel: {
|
||||
findAll: async ({ where }) =>
|
||||
docs.filter(
|
||||
(d) =>
|
||||
(where.status === undefined || d.status === where.status) &&
|
||||
(!where.id || where.id.includes(d.id)),
|
||||
),
|
||||
update: async (values, { where }) => {
|
||||
docs
|
||||
.filter((d) => where.id.includes(d.id))
|
||||
.forEach((d) => Object.assign(d, values));
|
||||
},
|
||||
destroy: async ({ where }) => removed.push(...where.id),
|
||||
},
|
||||
},
|
||||
'../config/util': {
|
||||
...util,
|
||||
concurrentRun: (tasks) => Promise.all(tasks.map((fn) => fn())),
|
||||
getGetCommand: (type, name) => {
|
||||
probes.push(name);
|
||||
return util.getGetCommand(type, name);
|
||||
},
|
||||
promiseExecSuccess: async () => '',
|
||||
},
|
||||
'../shared/pLimit': { removeQueuedDependency() {} },
|
||||
'./sock': class {},
|
||||
'../shared/i18n': {},
|
||||
}).default;
|
||||
const service = new Service({}, {});
|
||||
service.installDependenceOneByOne = (dependencies, install, force) => {
|
||||
uninstalls.push({ ids: dependencies.map((d) => d.id), install, force });
|
||||
return Promise.resolve();
|
||||
};
|
||||
return { service, probes, removed, uninstalls };
|
||||
}
|
||||
|
||||
test('listing preserves Python source results and still detects missing named packages', async () => {
|
||||
const docs = [
|
||||
' requirements with spaces/requirements.txt ',
|
||||
'requirements-dev.in',
|
||||
'project/pyproject.toml',
|
||||
'git+https://example.invalid/repo.git',
|
||||
'https://example.invalid/pkg.whl',
|
||||
'missing-package',
|
||||
].map((name, i) => ({ id: i + 1, name, type: 1, status: 1 }));
|
||||
const { service, probes } = serviceFor(docs);
|
||||
for (let i = 0; i < 2; i++) {
|
||||
await service.dependencies({
|
||||
searchValue: '',
|
||||
type: 'python3',
|
||||
status: '',
|
||||
});
|
||||
}
|
||||
assert.deepEqual(
|
||||
docs.map((d) => d.status),
|
||||
[1, 1, 1, 1, 1, 2],
|
||||
);
|
||||
assert.deepEqual(probes, ['missing-package']);
|
||||
// Source installs must continue to execute pip rather than treating the source as a package name.
|
||||
assert.equal(util.getGetCommand(1, docs[0].name), 'echo ""');
|
||||
});
|
||||
|
||||
test('deletion cleans previously misclassified Python sources and only queues intended records', async () => {
|
||||
const docs = [
|
||||
{ id: 1, name: 'requirements.txt', type: 1, status: 2 },
|
||||
{ id: 2, name: 'present-package', type: 0, status: 1 },
|
||||
{ id: 3, name: 'missing-package', type: 0, status: 2 },
|
||||
{ id: 4, name: 'queued/requirements.txt', type: 1, status: 6 },
|
||||
{ id: 5, name: 'cancelled/requirements.txt', type: 1, status: 7 },
|
||||
{ id: 6, name: 'git+https://example.invalid/repo.git', type: 1, status: 2 },
|
||||
];
|
||||
const { service, removed, uninstalls } = serviceFor(docs);
|
||||
await service.remove(docs.map((d) => d.id));
|
||||
assert.deepEqual(removed, [3, 4, 5]);
|
||||
assert.deepEqual(uninstalls, [
|
||||
{ ids: [1, 2, 6], install: false, force: false },
|
||||
]);
|
||||
assert.equal(docs[2].status, 2);
|
||||
});
|
||||
|
||||
test('Alpine container mirror command runs without sudo and writes valid repositories', async (t) => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ql-alpine-mirror-'));
|
||||
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
||||
const source =
|
||||
'https://dl-cdn.alpinelinux.org/alpine/v3.23/main\nhttps://dl-cdn.alpinelinux.org/alpine/v3.23/community\n';
|
||||
const api = load('back/config/util.ts', {
|
||||
'fs/promises': { readFile: async () => source },
|
||||
'./index': {},
|
||||
'./const': {},
|
||||
'./share': {},
|
||||
'./container': { isInContainer: () => true },
|
||||
'../loaders/logger': {},
|
||||
'../shared/utils': {},
|
||||
'../shared/fileAccess': {},
|
||||
'../data/dependence': {},
|
||||
});
|
||||
const previous = process.env.QL_OS_TYPE;
|
||||
let command;
|
||||
try {
|
||||
process.env.QL_OS_TYPE = 'alpine';
|
||||
command = await api.updateLinuxMirrorFile('https://mirror.example/alpine');
|
||||
} finally {
|
||||
if (previous === undefined) delete process.env.QL_OS_TYPE;
|
||||
else process.env.QL_OS_TYPE = previous;
|
||||
}
|
||||
// Redirect the generated command's paths and executable into the fixture only.
|
||||
const file = path.join(root, 'repositories');
|
||||
const update = path.join(root, 'updated');
|
||||
const sandbox = command
|
||||
.replaceAll('/etc/apk/repositories', file)
|
||||
.replaceAll('/etc/apk', root);
|
||||
execFileSync('/bin/bash', [
|
||||
'-c',
|
||||
`sudo(){ return 127; }; apk(){ test "$1" = update && touch '${update}'; }; ${sandbox}`,
|
||||
]);
|
||||
assert.ok(fs.existsSync(update));
|
||||
assert.equal(
|
||||
fs.readFileSync(file, 'utf8'),
|
||||
source.replaceAll(
|
||||
'https://dl-cdn.alpinelinux.org/alpine',
|
||||
'https://mirror.example/alpine',
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('force deletion removes a source record even when its requirements file is gone', async () => {
|
||||
const removed = [],
|
||||
updates = [];
|
||||
const Service = load('back/services/dependence.ts', {
|
||||
'../config': {},
|
||||
'../config/util': {
|
||||
getUninstallCommand() {
|
||||
throw Error('Python dependency file must be inside scripts or repo');
|
||||
},
|
||||
},
|
||||
'../data/dependence': {
|
||||
Dependence: class {},
|
||||
DependenceTypes: types,
|
||||
DependenceStatus: { ...statuses, removeFailed: 5 },
|
||||
DependenceModel: {
|
||||
destroy: async ({ where }) => removed.push(...where.id),
|
||||
update: async (values) => updates.push(values),
|
||||
},
|
||||
},
|
||||
'../shared/pLimit': {
|
||||
runDependeny: (_dep, fn) => fn(),
|
||||
removeQueuedDependency() {},
|
||||
},
|
||||
'./sock': class {},
|
||||
'../shared/i18n': {},
|
||||
}).default;
|
||||
const service = new Service({}, {});
|
||||
const dep = { id: 1, name: 'gone/requirements.txt', type: 1, status: 2 };
|
||||
await service.installOrUninstallDependency(dep, false, false);
|
||||
assert.deepEqual(removed, []);
|
||||
assert.equal(updates[0].status, 5);
|
||||
await service.installOrUninstallDependency(dep, false, true);
|
||||
assert.deepEqual(removed, [1]);
|
||||
});
|
||||
|
||||
test('Python extras run pip even when the base package version is already installed', async (t) => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ql-extra-install-'));
|
||||
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
||||
const marker = path.join(root, 'pip-ran');
|
||||
const dep = {
|
||||
id: 1,
|
||||
name: 'fixture[optional]>=1',
|
||||
type: 1,
|
||||
status: 6,
|
||||
log: [],
|
||||
};
|
||||
let probes = 0;
|
||||
const Service = load('back/services/dependence.ts', {
|
||||
'../config': {},
|
||||
'../config/util': {
|
||||
getInstallCommand: () => `printf installed > '${marker}'`,
|
||||
getGetCommand: () => {
|
||||
probes++;
|
||||
return 'probe';
|
||||
},
|
||||
promiseExecSuccess: async () => '1.0',
|
||||
detectOS: async () => 'Alpine',
|
||||
fileExist: async () => false,
|
||||
},
|
||||
'../data/dependence': {
|
||||
Dependence: class {},
|
||||
DependenceTypes: types,
|
||||
DependenceStatus: { ...statuses, installing: 0, cancelled: 7 },
|
||||
versionDependenceCommandTypes: { 0: '@', 1: '==', 2: '=' },
|
||||
DependenceModel: {
|
||||
findAll: async () => [dep],
|
||||
update: async (values) => Object.assign(dep, values),
|
||||
},
|
||||
},
|
||||
'../shared/pLimit': {
|
||||
firstDependencyId: 1,
|
||||
runDependeny: (_dep, fn) => fn(),
|
||||
removeQueuedDependency() {},
|
||||
updateDepLog: (fn) => fn(),
|
||||
},
|
||||
'./sock': class {},
|
||||
'../shared/i18n': { t: (text) => text, tf: (text) => text },
|
||||
}).default;
|
||||
const service = new Service({}, { sendMessage() {} });
|
||||
await service.installOrUninstallDependency(dep, true, false);
|
||||
assert.equal(probes, 0);
|
||||
assert.equal(fs.readFileSync(marker, 'utf8'), 'installed');
|
||||
assert.equal(dep.status, 1);
|
||||
});
|
||||
@@ -0,0 +1,127 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
const load = require('../helpers/load-security-module.cjs');
|
||||
|
||||
test('#3097 Python requirement probes preserve version constraints, extras and installed distributions', async (t) => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ql-specifiers-'));
|
||||
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
||||
const venv = path.join(root, 'virtual env');
|
||||
execFileSync('python3', ['-m', 'venv', venv]);
|
||||
execFileSync('python3', [
|
||||
'-c',
|
||||
`import zipfile,sys,os
|
||||
for version in ['1.2.3','2.1.0']:
|
||||
name='ql_specifier_fixture'
|
||||
with zipfile.ZipFile(os.path.join(sys.argv[1],name+'-'+version+'-py3-none-any.whl'),'w') as z:
|
||||
z.writestr(name+'.py','value=42\\n')
|
||||
info=name+'-'+version+'.dist-info/'
|
||||
z.writestr(info+'METADATA','Metadata-Version: 2.1\\nName: ql-specifier-fixture\\nVersion: '+version+'\\nProvides-Extra: optional\\n')
|
||||
z.writestr(info+'WHEEL','Wheel-Version: 1.0\\nGenerator: fixture\\nRoot-Is-Purelib: true\\nTag: py3-none-any\\n')
|
||||
z.writestr(info+'RECORD','')`,
|
||||
root,
|
||||
]);
|
||||
const api = load('back/config/util.ts', {
|
||||
'./index': { rootPath: root },
|
||||
'./const': { PYTHON_VENV_DIR: venv, PYTHON_INSTALL_DIR: '/unused-prefix' },
|
||||
'../loaders/logger': {},
|
||||
'../shared/utils': {},
|
||||
'../data/dependence': {
|
||||
DependenceTypes: { nodejs: 0, python3: 1, linux: 2 },
|
||||
},
|
||||
});
|
||||
const env = { ...process.env, PIP_NO_INDEX: '1', PIP_FIND_LINKS: root };
|
||||
for (const key of ['PYTHONPATH', 'PYTHONUSERBASE', 'PYTHON_HOME'])
|
||||
delete env[key];
|
||||
const run = (command) =>
|
||||
execFileSync('/bin/bash', ['-c', command], {
|
||||
encoding: 'utf8',
|
||||
env,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
}).trim();
|
||||
run(api.getInstallCommand(1, 'ql-specifier-fixture==1.2.3'));
|
||||
const Service = load('back/services/dependence.ts', {
|
||||
'../config': {},
|
||||
'../config/util': {
|
||||
...api,
|
||||
promiseExecSuccess: async (command) => run(command),
|
||||
},
|
||||
'../data/dependence': {
|
||||
Dependence: class {},
|
||||
DependenceTypes: { nodejs: 0, python3: 1, linux: 2 },
|
||||
versionDependenceCommandTypes: { 0: '@', 1: '==', 2: '=' },
|
||||
},
|
||||
'../shared/pLimit': {},
|
||||
'./sock': class {},
|
||||
'../shared/i18n': {},
|
||||
}).default;
|
||||
const service = new Service({}, {});
|
||||
assert.equal(
|
||||
await service.isDependenceInstalled({
|
||||
type: 1,
|
||||
name: 'ql-specifier-fixture==1.2.3.0',
|
||||
}),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
await service.isDependenceInstalled({
|
||||
type: 1,
|
||||
name: 'ql-specifier-fixture>=2',
|
||||
}),
|
||||
false,
|
||||
);
|
||||
for (const suffix of [
|
||||
'',
|
||||
'>=1.0',
|
||||
'<2',
|
||||
'>=1,<2',
|
||||
'~=1.2',
|
||||
'==1.*',
|
||||
'!=2.0',
|
||||
'[optional]>=1',
|
||||
'==1.2.3.0',
|
||||
]) {
|
||||
assert.equal(
|
||||
run(api.getGetCommand(1, 'ql-specifier-fixture' + suffix)),
|
||||
'1.2.3',
|
||||
suffix,
|
||||
);
|
||||
}
|
||||
for (const suffix of ['>=2', '<1', '!=1.2.3', '==1.2.30']) {
|
||||
assert.equal(
|
||||
run(api.getGetCommand(1, 'ql-specifier-fixture' + suffix)),
|
||||
'',
|
||||
suffix,
|
||||
);
|
||||
}
|
||||
assert.equal(run(api.getGetCommand(1, 'ql_specifier_fixture')), '1.2.3');
|
||||
assert.equal(run(api.getGetCommand(1, 'json')), 'json');
|
||||
assert.equal(
|
||||
run(api.getGetCommand(1, 'ql-missing-specifier-package>=1')),
|
||||
'',
|
||||
);
|
||||
assert.equal(
|
||||
run(api.getGetCommand(1, 'ql-specifier-fixture; python_version < "0"')),
|
||||
'not applicable',
|
||||
);
|
||||
run(api.getInstallCommand(1, 'ql-specifier-fixture>=2'));
|
||||
assert.equal(
|
||||
run(api.getGetCommand(1, 'ql-specifier-fixture>=2,<3')),
|
||||
'2.1.0',
|
||||
);
|
||||
run(api.getUninstallCommand(1, 'ql-specifier-fixture[optional]>=2,<3'));
|
||||
assert.equal(run(api.getGetCommand(1, 'ql-specifier-fixture')), '');
|
||||
assert.equal(fs.existsSync(path.join(root, 'INJECTED')), false);
|
||||
assert.equal(
|
||||
run(
|
||||
api.getGetCommand(
|
||||
1,
|
||||
`ql-specifier-fixture; __import__('os').system('touch ${root}/INJECTED')`,
|
||||
),
|
||||
),
|
||||
'',
|
||||
);
|
||||
});
|
||||
@@ -162,7 +162,7 @@ test('script discovery keeps filenames with spaces, glob characters and leading
|
||||
f.write(`scripts/${name}`, "new Env('fixture');\n");
|
||||
const result = f.run(
|
||||
extract('shell/otask.sh', 'gen_array_scripts') +
|
||||
'\ngen_array_scripts; printf "%s\\n" "${array_scripts[@]}"',
|
||||
'\ngen_array_scripts; printf "%s\\n" "${array_scripts[@]}"; printf "NAME=%s\\n" "${array_scripts_name[@]}"',
|
||||
{
|
||||
dir_scripts: path.join(f.root, 'scripts'),
|
||||
},
|
||||
@@ -171,6 +171,8 @@ test('script discovery keeps filenames with spaces, glob characters and leading
|
||||
assert.equal(result.stderr, '');
|
||||
assert.deepEqual(result.stdout.trim().split('\n').sort(), [
|
||||
'-dash name.js',
|
||||
'NAME=fixture',
|
||||
'NAME=fixture',
|
||||
'probe [1].js',
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
test('notification log column titles resolve in every supported locale', () => {
|
||||
const source = fs.readFileSync(
|
||||
path.join(__dirname, '../../src/pages/setting/notifyLog.tsx'),
|
||||
'utf8',
|
||||
);
|
||||
const titles = [...source.matchAll(/title:\s*intl\.get\('([^']+)'\)/g)].map(
|
||||
(m) => m[1],
|
||||
);
|
||||
assert.ok(titles.includes('标题') && titles.includes('内容'));
|
||||
for (const locale of ['zh-CN', 'en-US']) {
|
||||
const messages = require(`../../src/locales/${locale}.json`);
|
||||
for (const title of titles)
|
||||
assert.ok(messages[title]?.trim(), `${locale}: ${title}`);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user