mirror of
https://github.com/angular/angular-cli.git
synced 2025-05-23 07:19:58 +08:00
G3 is now using RXJS version 7 which makes it possible for the CLI to also be updated to RXJS 7. NB: this change does not remove all usages of the deprecated APIs. Closes #24371
436 lines
12 KiB
TypeScript
436 lines
12 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright Google LLC All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://angular.io/license
|
|
*/
|
|
|
|
import { json, logging, schema } from '@angular-devkit/core';
|
|
import { firstValueFrom, lastValueFrom, map, take, tap, timer, toArray } from 'rxjs';
|
|
import { promisify } from 'util';
|
|
import { TestingArchitectHost } from '../testing/testing-architect-host';
|
|
import { BuilderOutput, BuilderRun } from './api';
|
|
import { Architect } from './architect';
|
|
import { createBuilder } from './create-builder';
|
|
|
|
const flush = promisify(setImmediate);
|
|
|
|
describe('architect', () => {
|
|
let testArchitectHost: TestingArchitectHost;
|
|
let architect: Architect;
|
|
let called = 0;
|
|
let options: {} = {};
|
|
const target1 = {
|
|
project: 'test',
|
|
target: 'test',
|
|
};
|
|
const target2 = {
|
|
project: 'test',
|
|
target: 'abc',
|
|
};
|
|
|
|
beforeEach(async () => {
|
|
const registry = new schema.CoreSchemaRegistry();
|
|
registry.addPostTransform(schema.transforms.addUndefinedDefaults);
|
|
testArchitectHost = new TestingArchitectHost();
|
|
architect = new Architect(testArchitectHost, registry);
|
|
|
|
options = {};
|
|
called = 0;
|
|
testArchitectHost.addBuilder(
|
|
'package:test',
|
|
createBuilder(async (o) => {
|
|
called++;
|
|
options = o;
|
|
|
|
return new Promise<BuilderOutput>((resolve) => {
|
|
setTimeout(() => resolve({ success: true }), 10);
|
|
});
|
|
}),
|
|
);
|
|
testArchitectHost.addBuilder(
|
|
'package:test-options',
|
|
createBuilder((o) => {
|
|
options = o;
|
|
|
|
return { success: true };
|
|
}),
|
|
);
|
|
|
|
testArchitectHost.addTarget(target1, 'package:test');
|
|
testArchitectHost.addTarget(target2, 'package:test');
|
|
});
|
|
|
|
it('works', async () => {
|
|
testArchitectHost.addBuilder(
|
|
'package:test',
|
|
createBuilder(() => ({ success: true })),
|
|
);
|
|
|
|
const run = await architect.scheduleBuilder('package:test', {});
|
|
expect(await run.result).toEqual(jasmine.objectContaining({ success: true }));
|
|
await run.stop();
|
|
});
|
|
|
|
it('works with async builders', async () => {
|
|
testArchitectHost.addBuilder(
|
|
'package:test',
|
|
createBuilder(async () => ({ success: true })),
|
|
);
|
|
|
|
const run = await architect.scheduleBuilder('package:test', {});
|
|
expect(await run.result).toEqual(jasmine.objectContaining({ success: true }));
|
|
await run.stop();
|
|
});
|
|
|
|
it('supports async generator builders', async () => {
|
|
testArchitectHost.addBuilder(
|
|
'package:test',
|
|
createBuilder(async function* () {
|
|
yield { success: true };
|
|
}),
|
|
);
|
|
|
|
const run = await architect.scheduleBuilder('package:test', {});
|
|
expect(await run.result).toEqual(jasmine.objectContaining({ success: true }));
|
|
await run.stop();
|
|
});
|
|
|
|
it('runs builders parallel', async () => {
|
|
const run = await architect.scheduleBuilder('package:test', {});
|
|
const run2 = await architect.scheduleBuilder('package:test', {});
|
|
await flush();
|
|
expect(called).toBe(2);
|
|
expect(await run.result).toEqual(jasmine.objectContaining({ success: true }));
|
|
expect(await run2.result).toEqual(jasmine.objectContaining({ success: true }));
|
|
expect(called).toBe(2);
|
|
await run.stop();
|
|
});
|
|
|
|
it('runs targets parallel', async () => {
|
|
const run = await architect.scheduleTarget(target1, {});
|
|
const run2 = await architect.scheduleTarget(target1, {});
|
|
await flush();
|
|
expect(called).toBe(2);
|
|
expect(await run.result).toEqual(jasmine.objectContaining({ success: true }));
|
|
expect(await run2.result).toEqual(jasmine.objectContaining({ success: true }));
|
|
expect(called).toBe(2);
|
|
await run.stop();
|
|
});
|
|
|
|
it('passes options to builders', async () => {
|
|
const o = { helloBuilder: 'world' };
|
|
const run = await architect.scheduleBuilder('package:test-options', o);
|
|
expect(await run.result).toEqual(jasmine.objectContaining({ success: true }));
|
|
expect(options).toEqual(o);
|
|
await run.stop();
|
|
});
|
|
|
|
it('passes options to targets', async () => {
|
|
const o = { helloTarget: 'world' };
|
|
const run = await architect.scheduleTarget(target1, o);
|
|
expect(await run.result).toEqual(jasmine.objectContaining({ success: true }));
|
|
expect(options).toEqual(o);
|
|
await run.stop();
|
|
});
|
|
|
|
it(`errors when target configuration does not exist`, async () => {
|
|
await expectAsync(architect.scheduleBuilder('test:test:invalid', {})).toBeRejectedWithError(
|
|
'Job name "test:test:invalid" does not exist.',
|
|
);
|
|
});
|
|
|
|
it('errors when builder cannot be resolved', async () => {
|
|
try {
|
|
await architect.scheduleBuilder('non:existent', {});
|
|
expect('to throw').not.toEqual('to throw');
|
|
} catch {}
|
|
});
|
|
|
|
it('works with watching observable builders', async () => {
|
|
let results = 0;
|
|
testArchitectHost.addBuilder(
|
|
'package:test-watch',
|
|
createBuilder((_, context) => {
|
|
called++;
|
|
|
|
return timer(10, 10).pipe(
|
|
take(10),
|
|
map(() => {
|
|
context.reportRunning();
|
|
|
|
return { success: true };
|
|
}),
|
|
tap(() => results++),
|
|
);
|
|
}),
|
|
);
|
|
|
|
const run = await architect.scheduleBuilder('package:test-watch', {});
|
|
await run.result;
|
|
expect(called).toBe(1);
|
|
expect(results).toBe(1);
|
|
|
|
const all = await lastValueFrom(run.output.pipe(toArray()));
|
|
expect(called).toBe(1);
|
|
expect(results).toBe(10);
|
|
expect(all.length).toBe(10);
|
|
});
|
|
|
|
it('works with watching async generator builders', async () => {
|
|
let results = 0;
|
|
testArchitectHost.addBuilder(
|
|
'package:test-watch-gen',
|
|
createBuilder(async function* (_, context) {
|
|
called++;
|
|
|
|
for (let x = 0; x < 10; x++) {
|
|
await new Promise(setImmediate);
|
|
context.reportRunning();
|
|
yield { success: true };
|
|
results++;
|
|
}
|
|
}),
|
|
);
|
|
|
|
const run = await architect.scheduleBuilder('package:test-watch-gen', {});
|
|
await run.result;
|
|
expect(called).toBe(1);
|
|
expect(results).toBe(1);
|
|
|
|
const all = await lastValueFrom(run.output.pipe(toArray()));
|
|
expect(called).toBe(1);
|
|
expect(results).toBe(10);
|
|
expect(all.length).toBe(10);
|
|
});
|
|
|
|
it('propagates all logging entries', async () => {
|
|
const logCount = 100;
|
|
|
|
testArchitectHost.addBuilder(
|
|
'package:test-logging',
|
|
createBuilder(async (_, context) => {
|
|
for (let i = 0; i < logCount; ++i) {
|
|
context.logger.info(i.toString());
|
|
}
|
|
|
|
return { success: true };
|
|
}),
|
|
);
|
|
|
|
const logger = new logging.Logger('test-logger');
|
|
const logs: string[] = [];
|
|
logger.subscribe({
|
|
next(entry) {
|
|
logs.push(entry.message);
|
|
},
|
|
});
|
|
const run = await architect.scheduleBuilder('package:test-logging', {}, { logger });
|
|
expect(await run.result).toEqual(jasmine.objectContaining({ success: true }));
|
|
await run.stop();
|
|
|
|
for (let i = 0; i < logCount; ++i) {
|
|
expect(logs[i]).toBe(i.toString());
|
|
}
|
|
});
|
|
|
|
it('reports errors in the builder', async () => {
|
|
testArchitectHost.addBuilder(
|
|
'package:error',
|
|
createBuilder(() => {
|
|
throw new Error('Error in the builder.');
|
|
}),
|
|
);
|
|
|
|
let run: BuilderRun | undefined = undefined;
|
|
try {
|
|
try {
|
|
// This should not throw.
|
|
run = await architect.scheduleBuilder('package:error', {});
|
|
} catch (err) {
|
|
expect(err).toBeUndefined();
|
|
throw err;
|
|
}
|
|
|
|
// This should throw.
|
|
await run.result;
|
|
expect('to throw').not.toEqual('to throw');
|
|
} catch {}
|
|
if (run) {
|
|
await run.stop();
|
|
}
|
|
});
|
|
|
|
it('reports errors in the builder (async)', async () => {
|
|
testArchitectHost.addBuilder(
|
|
'package:error',
|
|
createBuilder(() => {
|
|
return Promise.reject(new Error('Error async'));
|
|
}),
|
|
);
|
|
|
|
let run: BuilderRun | undefined = undefined;
|
|
try {
|
|
try {
|
|
// This should not throw.
|
|
run = await architect.scheduleBuilder('package:error', {});
|
|
} catch (err) {
|
|
expect(err).toBeUndefined();
|
|
throw err;
|
|
}
|
|
|
|
// This should throw.
|
|
await run.result;
|
|
expect('to throw').not.toEqual('to throw');
|
|
} catch {}
|
|
if (run) {
|
|
await run.stop();
|
|
}
|
|
});
|
|
|
|
it('reports errors in options', async () => {
|
|
const builderName = 'options:error';
|
|
const builder = createBuilder(() => ({ success: true }));
|
|
const optionSchema = { type: 'object', additionalProperties: false };
|
|
testArchitectHost.addBuilder(builderName, builder, '', optionSchema);
|
|
|
|
const run = await architect.scheduleBuilder(builderName, { extraProp: true });
|
|
await expectAsync(run.result).toBeRejectedWith(
|
|
jasmine.objectContaining({ message: jasmine.stringMatching('extraProp') }),
|
|
);
|
|
await run.stop();
|
|
});
|
|
|
|
it('exposes getTargetOptions() properly', async () => {
|
|
const goldenOptions = {
|
|
value: 'value',
|
|
};
|
|
let options = {} as object;
|
|
|
|
const target = {
|
|
project: 'project',
|
|
target: 'target',
|
|
};
|
|
testArchitectHost.addTarget(target, 'package:target', goldenOptions);
|
|
|
|
testArchitectHost.addBuilder(
|
|
'package:getTargetOptions',
|
|
createBuilder(async (_, context) => {
|
|
options = await context.getTargetOptions(target);
|
|
|
|
return { success: true };
|
|
}),
|
|
);
|
|
|
|
const run = await architect.scheduleBuilder('package:getTargetOptions', {});
|
|
const output = await lastValueFrom(run.output);
|
|
expect(output.success).toBe(true);
|
|
expect(options).toEqual(goldenOptions);
|
|
await run.stop();
|
|
|
|
// Use an invalid target and check for error.
|
|
target.target = 'invalid';
|
|
options = {};
|
|
|
|
// This should not error.
|
|
const run2 = await architect.scheduleBuilder('package:getTargetOptions', {});
|
|
|
|
// But this should.
|
|
try {
|
|
await lastValueFrom(run2.output);
|
|
expect('THE ABOVE LINE SHOULD NOT ERROR').toBe('false');
|
|
} catch {}
|
|
await run2.stop();
|
|
});
|
|
|
|
it('exposes getBuilderNameForTarget()', async () => {
|
|
const builderName = 'ImBlue:DabadeeDabada';
|
|
testArchitectHost.addBuilder(
|
|
builderName,
|
|
createBuilder(() => ({ success: true })),
|
|
);
|
|
|
|
const target = {
|
|
project: 'some-project',
|
|
target: 'some-target',
|
|
};
|
|
testArchitectHost.addTarget(target, builderName);
|
|
|
|
let actualBuilderName = '';
|
|
testArchitectHost.addBuilder(
|
|
'package:do-it',
|
|
createBuilder(async (_, context) => {
|
|
actualBuilderName = await context.getBuilderNameForTarget(target);
|
|
|
|
return { success: true };
|
|
}),
|
|
);
|
|
|
|
const run = await architect.scheduleBuilder('package:do-it', {});
|
|
const output = await lastValueFrom(run.output);
|
|
expect(output.success).toBe(true);
|
|
expect(actualBuilderName).toEqual(builderName);
|
|
await run.stop();
|
|
|
|
// Use an invalid target and check for error.
|
|
target.target = 'invalid';
|
|
actualBuilderName = '';
|
|
|
|
// This should not error.
|
|
const run2 = await architect.scheduleBuilder('package:do-it', {});
|
|
|
|
// But this should.
|
|
try {
|
|
await lastValueFrom(run2.output);
|
|
expect('THE ABOVE LINE SHOULD NOT ERROR').toBe('false');
|
|
} catch {}
|
|
await run2.stop();
|
|
});
|
|
|
|
it('exposes validateOptions()', async () => {
|
|
const builderName = 'Hello:World';
|
|
testArchitectHost.addBuilder(
|
|
builderName,
|
|
createBuilder(() => ({ success: true })),
|
|
'',
|
|
{
|
|
type: 'object',
|
|
properties: {
|
|
p0: { type: 'number', default: 123 },
|
|
p1: { type: 'string' },
|
|
},
|
|
required: ['p1'],
|
|
},
|
|
);
|
|
|
|
let actualOptions: json.JsonObject = {};
|
|
testArchitectHost.addBuilder(
|
|
'package:do-it',
|
|
createBuilder(async (options, context) => {
|
|
actualOptions = await context.validateOptions(options, builderName);
|
|
|
|
return { success: true };
|
|
}),
|
|
);
|
|
|
|
const run = await architect.scheduleBuilder('package:do-it', { p1: 'hello' });
|
|
const output = await firstValueFrom(run.output);
|
|
expect(output.success).toBe(true);
|
|
expect(actualOptions).toEqual({
|
|
p0: 123,
|
|
p1: 'hello',
|
|
});
|
|
await run.stop();
|
|
|
|
// Should also error.
|
|
const run2 = await architect.scheduleBuilder('package:do-it', {});
|
|
|
|
await expectAsync(lastValueFrom(run2.output)).toBeRejectedWith(
|
|
jasmine.objectContaining({ message: jasmine.stringMatching('p1') }),
|
|
);
|
|
|
|
await run2.stop();
|
|
});
|
|
});
|