appium-espresso-driver
Version:
Espresso integration for Appium
218 lines • 9.24 kB
JavaScript
import { SubProcess } from 'teen_process';
import { fs, system } from 'appium/support.js';
import path from 'node:path';
import { EOL } from 'node:os';
import { escapeRegExp } from '../../utils/index.js';
const GRADLE_VERSION_KEY = 'gradle';
const GRADLE_URL_PREFIX = 'distributionUrl=';
export const GRADLE_URL_TEMPLATE = 'https\\://services.gradle.org/distributions/gradle-VERSION-all.zip';
const DEPENDENCY_PROP_NAMES = [
'additionalAppDependencies',
'additionalAndroidTestDependencies',
];
export const VERSION_KEYS = [
GRADLE_VERSION_KEY,
'androidGradlePlugin',
'compileSdk',
'buildTools',
'minSdk',
'targetSdk',
'kotlin',
'sourceCompatibility',
'targetCompatibility',
'jvmTarget',
'composeVersion',
'espressoVersion',
'annotationVersion',
];
export class ServerBuilder {
log;
serverPath;
showGradleLog;
serverVersions;
testAppPackage;
signingConfig;
additionalAppDependencies = [];
additionalAndroidTestDependencies = [];
composeSupport;
constructor(log, args) {
this.log = log;
this.serverPath = args.serverPath;
this.showGradleLog = args.showGradleLog;
const buildConfiguration = args.buildConfiguration || {};
this.composeSupport = buildConfiguration.composeSupport !== false;
const versionConfiguration = buildConfiguration.toolsVersions || {};
this.serverVersions = {};
for (const [key, value] of Object.entries(versionConfiguration)) {
if (VERSION_KEYS.includes(key)) {
this.serverVersions[key] = value;
}
else {
log.warn(`Got unexpected '${key}' in toolsVersion block of the build configuration`);
}
}
this.testAppPackage = args.testAppPackage;
this.signingConfig = args.signingConfig;
for (const propName of DEPENDENCY_PROP_NAMES) {
this[propName] = buildConfiguration[propName] || [];
}
}
async build() {
const gradleVersion = this.serverVersions[GRADLE_VERSION_KEY];
if (gradleVersion) {
await this.setGradleWrapperVersion(gradleVersion);
}
await this.insertAdditionalDependencies();
await this.runBuildProcess();
}
getCommand() {
const cmd = system.isWindows() ? 'gradlew.bat' : path.resolve(this.serverPath, 'gradlew');
const buildProperty = (key, value) => value ? `-P${key}=${value}` : null;
const args = VERSION_KEYS.filter((key) => key !== GRADLE_VERSION_KEY)
.map((key) => {
const serverVersion = this.serverVersions[key];
const gradleProperty = `appium${key.charAt(0).toUpperCase()}${key.slice(1)}`;
return buildProperty(gradleProperty, serverVersion);
})
.filter((arg) => typeof arg === 'string' && Boolean(arg));
const signingConfig = this.signingConfig;
if (signingConfig) {
args.push(...Object.keys(signingConfig)
.map((key) => {
const propKey = key;
const propValue = signingConfig[propKey];
const k = `appium${key.charAt(0).toUpperCase()}${key.slice(1)}`;
const v = propValue != null ? String(propValue) : undefined;
return buildProperty(k, v);
})
.filter((arg) => typeof arg === 'string' && Boolean(arg)));
}
if (this.testAppPackage) {
const targetPackageArg = buildProperty('appiumTargetPackage', this.testAppPackage);
if (targetPackageArg) {
args.push(targetPackageArg);
}
}
if (!this.composeSupport) {
const composeArg = buildProperty('appiumComposeSupport', 'false');
if (composeArg) {
args.push(composeArg);
}
}
args.push('app:assembleAndroidTest');
return { cmd, args };
}
async setGradleWrapperVersion(version) {
const propertiesPath = path.resolve(this.serverPath, 'gradle', 'wrapper', 'gradle-wrapper.properties');
const originalProperties = await fs.readFile(propertiesPath, 'utf8');
const newProperties = this.updateGradleDistUrl(originalProperties, version);
await fs.writeFile(propertiesPath, newProperties, 'utf8');
}
updateGradleDistUrl(propertiesContent, version) {
return propertiesContent.replace(new RegExp(`^(${escapeRegExp(GRADLE_URL_PREFIX)}).+$`, 'gm'), `$1${GRADLE_URL_TEMPLATE.replace('VERSION', version)}`);
}
async insertAdditionalDependencies() {
let hasAdditionalDeps = false;
for (const propName of DEPENDENCY_PROP_NAMES) {
const deps = this[propName];
if (!Array.isArray(deps)) {
throw new Error(`'${propName}' must be an array`);
}
if (deps.filter((line) => line.trim()).length === 0) {
continue;
}
for (const dep of deps) {
if (/[\s'\\$]/.test(dep)) {
throw new Error('Single quotes, dollar characters and whitespace characters' +
` are disallowed in additional dependencies: ${dep}`);
}
}
hasAdditionalDeps = true;
}
if (!hasAdditionalDeps) {
return;
}
const buildPath = path.resolve(this.serverPath, 'app', 'build.gradle.kts');
let configuration = await fs.readFile(buildPath, 'utf8');
for (const propName of DEPENDENCY_PROP_NAMES) {
const prefix = propName === DEPENDENCY_PROP_NAMES[0] ? 'api' : 'androidTestImplementation';
const deps = this[propName]
.filter((line) => line.trim())
.map((line) => `${prefix}("${line}")`);
if (deps.length === 0) {
continue;
}
this.log.info(`Adding the following ${propName} to build.gradle.kts: ${deps}`);
configuration = updateDependencyLines(configuration, propName, deps);
}
await fs.writeFile(buildPath, configuration, 'utf8');
}
async runBuildProcess() {
const { cmd, args } = this.getCommand();
this.log.debug(`Beginning build with command '${cmd} ${args.join(' ')}' ` +
`in directory '${this.serverPath}'`);
const gradlebuild = new SubProcess(cmd, args, {
cwd: this.serverPath,
stdio: ['ignore', 'pipe', 'pipe'],
// https://github.com/nodejs/node/issues/52572
shell: system.isWindows(),
windowsVerbatimArguments: true,
});
const gradleError = [];
const logMsg = `Output from Gradle ${this.showGradleLog ? 'will' : 'will not'} be logged`;
this.log.debug(`${logMsg}. To change this, use 'showGradleLog' desired capability`);
gradlebuild.on('line-stderr', (line) => {
this.log.warn(`[Gradle] ${line}`);
gradleError.push(line);
});
gradlebuild.on('line-stdout', (line) => this.log.info(`[Gradle] ${line}`));
try {
await gradlebuild.start();
await gradlebuild.join();
}
catch (err) {
const msg = `Unable to build Espresso server - ${err.message}\n` +
`Gradle error message:${EOL}${gradleError.join('\n')}`;
throw this.log.errorWithException(msg);
}
finally {
gradlebuild.removeAllListeners();
}
}
}
/**
* Build keystore options for signing the built Espresso server test APK.
*
* @param args - Paths and credentials for the keystore used when signing.
* @returns Configuration consumed by the Gradle signing step.
*/
export function buildServerSigningConfig(args) {
return {
zipAlign: true,
keystoreFile: args.keystoreFile,
keystorePassword: args.keystorePassword,
keyAlias: args.keyAlias,
keyPassword: args.keyPassword,
};
}
/**
* Insert Gradle dependency lines after a `// placeholder` marker in a Gradle file.
*
* @param originalContent - Full text of the Gradle configuration file.
* @param dependencyPlaceholder - Placeholder comment label to find (e.g. from build.gradle).
* @param dependencyLines - Dependency lines to insert (e.g. `implementation "..."`).
* @returns Updated file content, or the original string if the placeholder is missing.
*/
export function updateDependencyLines(originalContent, dependencyPlaceholder, dependencyLines) {
const configurationLines = originalContent.split('\n');
const searchRe = new RegExp(`^\\s*//\\s*\\b${escapeRegExp(dependencyPlaceholder)}\\b`, 'm');
const placeholderIndex = configurationLines.findIndex((line) => searchRe.test(line));
if (placeholderIndex < 0) {
return originalContent;
}
const placeholderLine = configurationLines[placeholderIndex];
const indentLen = placeholderLine.length - placeholderLine.trimStart().length;
configurationLines.splice(placeholderIndex + 1, 0, ...dependencyLines.map((line) => `${' '.repeat(indentLen)}${line}`));
return configurationLines.join('\n');
}
//# sourceMappingURL=builder.js.map