Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ export const PLATFORM_NAME_IOS = 'iOS';

export const SDK_DEVICE = 'iphoneos';

/**
* @deprecated The WDA upgrade timestamp path is no longer used.
*/
export const WDA_UPGRADE_TIMESTAMP_PATH = path.join('.appium', 'webdriveragent', 'upgrade.time');

/**
Expand Down
39 changes: 11 additions & 28 deletions lib/webdriveragent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,7 @@ import type {AppiumLogger, StringRecord} from '@appium/types';
import AsyncLock from 'async-lock';
import {waitForCondition} from 'asyncbox';

import {
WDA_RUNNER_BUNDLE_ID,
WDA_BASE_URL,
WDA_UPGRADE_TIMESTAMP_PATH,
DEFAULT_TEST_BUNDLE_SUFFIX,
} from './constants.js';
import {WDA_RUNNER_BUNDLE_ID, WDA_BASE_URL, DEFAULT_TEST_BUNDLE_SUFFIX} from './constants.js';
import {log as defaultLogger} from './logger.js';
import {NoSessionProxy} from './no-session-proxy.js';
import type {
Expand Down Expand Up @@ -584,31 +579,19 @@ export class WebDriverAgent {

const packageInfo = JSON.parse(await fs.readFile(path.join(BOOTSTRAP_PATH, 'package.json'), 'utf8'));
const box = strongbox(packageInfo.name);
let boxItem = box.getItem(RECENT_MODULE_VERSION_ITEM_NAME);
if (!boxItem) {
const timestampPath = path.resolve(process.env.HOME ?? '', WDA_UPGRADE_TIMESTAMP_PATH);
if (await fs.exists(timestampPath)) {
// TODO: It is probably a bit ugly to hardcode the recent version string,
// TODO: hovewer it should do the job as a temporary transition trick
// TODO: to switch from a hardcoded file path to the strongbox usage.
try {
boxItem = await box.createItemWithValue(RECENT_MODULE_VERSION_ITEM_NAME, '5.0.0');
} catch (e: any) {
this.log.warn(`The actual module version cannot be persisted: ${e.message}`);
return;
}
} else {
this.log.info('There is no need to perform the project cleanup. A fresh install has been detected');
try {
await box.createItemWithValue(RECENT_MODULE_VERSION_ITEM_NAME, packageInfo.version);
} catch (e: any) {
this.log.warn(`The actual module version cannot be persisted: ${e.message}`);
}
return;
// Each Strongbox instance starts with an empty item map. Load the persisted value from disk.
const boxItem = await box.createItem<string>(RECENT_MODULE_VERSION_ITEM_NAME);
let recentModuleVersion = boxItem.value;
if (recentModuleVersion === undefined) {
Comment thread
KazuCocoa marked this conversation as resolved.
this.log.info('There is no need to perform the project cleanup. A fresh install has been detected');
try {
await boxItem.write(packageInfo.version);
} catch (e: any) {
this.log.warn(`The actual module version cannot be persisted: ${e.message}`);
}
return;
}

let recentModuleVersion = await boxItem.read();
try {
recentModuleVersion = util.coerceVersion(recentModuleVersion, true);
} catch (e: any) {
Expand Down
136 changes: 136 additions & 0 deletions test/unit/wda-cleanup.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import assert from 'node:assert/strict';
import {mkdtemp, readFile, rm} from 'node:fs/promises';
import {tmpdir} from 'node:os';
import path from 'node:path';
import {afterEach, beforeEach, describe, it, mock} from 'node:test';

import {strongbox} from '@appium/strongbox';
import {fs} from '@appium/support';
import sinon from 'sinon';

import {BOOTSTRAP_PATH} from '../../lib/utils/index.js';

let container: string;
function isolatedStrongbox(name: string) {
const box = strongbox(name);
// Preserve the temporary path verbatim instead of Strongbox's container slugification.
Object.defineProperty(box, 'container', {value: container});
return box;
}
mock.module('@appium/strongbox', {
namedExports: {
strongbox: isolatedStrongbox,
},
});
const {WebDriverAgent} = await import('../../lib/webdriveragent.js');
const packageInfo = JSON.parse(await readFile(path.join(BOOTSTRAP_PATH, 'package.json'), 'utf8'));
const itemName = 'recentWdaModuleVersion';

describe('WDA project cleanup persistence', function () {
let sandbox: sinon.SinonSandbox;
let legacyMarker: sinon.SinonStub;

beforeEach(async function () {
container = await mkdtemp(path.join(tmpdir(), 'wda-cleanup-'));
sandbox = sinon.createSandbox();
legacyMarker = sandbox
.stub(fs, 'exists')
.withArgs(path.resolve(process.env.HOME ?? '', '.appium', 'webdriveragent', 'upgrade.time'))
.resolves(false);
});

afterEach(async function () {
sandbox.restore();
await rm(container, {recursive: true, force: true});
});

function newAgent() {
const agent = new WebDriverAgent({device: {udid: 'test-udid'}, platformVersion: '17.2'});
const clean = sandbox.stub(agent.xcodebuild, 'cleanProject').resolves();
return {clean, run: async () => await (agent as any)._cleanupProjectIfFresh()};
}

async function persist(version: string) {
await isolatedStrongbox(packageInfo.name).createItemWithValue(itemName, version);
}

async function persistedVersion() {
return (await isolatedStrongbox(packageInfo.name).createItem<string>(itemName)).value;
}
Comment thread
KazuCocoa marked this conversation as resolved.

it('reuses the persisted version despite a legacy marker across new agents', async function () {
legacyMarker.resolves(true);
await persist(packageInfo.version);
for (let i = 0; i < 2; i++) {
const agent = newAgent();
await agent.run();
sandbox.assert.notCalled(agent.clean);
}
sandbox.assert.notCalled(legacyMarker);
assert.equal(await persistedVersion(), packageInfo.version);
});

it('cleans an older persisted version once without a legacy marker', async function () {
await persist('5.0.0');
const first = newAgent();
await first.run();
sandbox.assert.calledOnce(first.clean);
assert.equal(await persistedVersion(), packageInfo.version);
const second = newAgent();
await second.run();
sandbox.assert.notCalled(second.clean);
sandbox.assert.notCalled(legacyMarker);
});

it('initializes missing version state without consulting a legacy marker', async function () {
legacyMarker.resolves(true);
const first = newAgent();
await first.run();
sandbox.assert.notCalled(first.clean);
assert.equal(await persistedVersion(), packageInfo.version);
const second = newAgent();
await second.run();
sandbox.assert.notCalled(second.clean);
sandbox.assert.notCalled(legacyMarker);
});

it('initializes a fresh installation without cleaning', async function () {
for (let i = 0; i < 2; i++) {
const agent = newAgent();
await agent.run();
sandbox.assert.notCalled(agent.clean);
}
sandbox.assert.notCalled(legacyMarker);
assert.equal(await persistedVersion(), packageInfo.version);
});

it('preserves a newer persisted version', async function () {
await persist('999.0.0');
const agent = newAgent();
await agent.run();
sandbox.assert.notCalled(agent.clean);
assert.equal(await persistedVersion(), '999.0.0');
});

it('repairs a damaged persisted version without treating it as legacy state', async function () {
legacyMarker.resolves(true);
await persist('not-a-version');
const agent = newAgent();
await agent.run();
sandbox.assert.notCalled(agent.clean);
sandbox.assert.notCalled(legacyMarker);
assert.equal(await persistedVersion(), packageInfo.version);
});

it('retries cleanup after a failure without recording a successful upgrade', async function () {
await persist('5.0.0');
const first = newAgent();
first.clean.rejects(new Error('cleanup failed'));
await first.run();
assert.equal(await persistedVersion(), '5.0.0');
const second = newAgent();
await second.run();
sandbox.assert.calledOnce(second.clean);
assert.equal(await persistedVersion(), packageInfo.version);
});
});
Loading