Skip to content

Conversation

FDIM
Copy link
Contributor

@FDIM FDIM commented Jan 12, 2019

A new --include option is added. It is a glob/pattern that specifies which specs should be included in the build.

Command:
ng test --include=app/services/auth*

Few rules are applied to the pattern to make certain things possible:

  • .spec.ts is added automatically if missing (less typing)
  • if it ends with *.ts and doesn't have .spec.ts, extension will be replaced with .spec.ts
  • if path starts at root, source root directory will be trimmed. So that you could use relative file path from editor to make custom tasks to run the command

Behind the scenes test.ts is transformed which instead of using require.context imports all matching files. This gives quite a big boost in the project at work (at least 10s on each iterative build!)

Fixes #3603

Motivation: It is a pain in a big application to wait until entire app is rebuilt just to run single fit block. Takes almost a minute with 4000+ tests and barely works when code coverage is enabled.

Missing: docs(?)

Example tasks for vscode:

{
  // See https://go.microsoft.com/fwlink/?LinkId=733558
  // for the documentation about the tasks.json format
  "version": "2.0.0",
  "tasks": [
    {
      "type": "shell",
      "label": "test selected spec",
      "command": "node_modules/.bin/ng",
      "args": [
        "test",
        "--include",
        "${relativeFile}"
      ],
      "problemMatcher": []
    }
  ]
}

PS. The setup for writing tests is quite nice!

@FDIM
Copy link
Contributor Author

FDIM commented Jan 12, 2019

Original PR (#12955) was closed because of inactivity - 1 day too soon. This one is as ready as I can make it without input

@FDIM FDIM force-pushed the feature/running-selected-specs branch 2 times, most recently from b9badfa to f3cd052 Compare January 12, 2019 17:25
@ngbot
Copy link

ngbot bot commented Jan 18, 2019

Hi @FDIM! This PR has merge conflicts due to recent upstream merges.
Please help to unblock it by resolving these conflicts. Thanks!

@alan-agius4 alan-agius4 added the needs: discussion On the agenda for team meeting to determine next steps label Jan 19, 2019
@FDIM FDIM force-pushed the feature/running-selected-specs branch from f3cd052 to e8943f3 Compare January 20, 2019 13:33
@filipesilva filipesilva self-assigned this Jan 24, 2019
@hansl hansl removed the needs: discussion On the agenda for team meeting to determine next steps label Jan 24, 2019
@FDIM
Copy link
Contributor Author

FDIM commented Jan 28, 2019

@filipesilva let me know if you need any help with pushing this futher/making solution better

@filipesilva
Copy link
Contributor

Heya @FDIM, thank you for tackling this feature! I want to give you an update on the status of this PR.

We're looking at the feature as you have designed it so far and thinking about the pros and cons. We're also considering what extra work needs to be done to update projects to use it (e.g. ignoring the generated file). I should have some news soon as I drive it forward within the team!

@filipesilva filipesilva added the needs: discussion On the agenda for team meeting to determine next steps label Feb 4, 2019
@mgechev mgechev removed the needs: discussion On the agenda for team meeting to determine next steps label Feb 7, 2019
@filipesilva
Copy link
Contributor

@FDIM heya, we got together and discussed this PR yesterday. This PR adds two new pieces of functionality: running targeted specs, and changing the targeted spec of a running process. We feel there's a lot of value in the former, but comparatively less so in the latter since it's a subset of the usescases of the first.

The main gripe the team has with it is the temporary file. In this PR the temporary file is used to transform the original test code via regexes running it, and communicate between processes when updating the targeted spec

Creating temporary files on disk has a few problems though:

  • requires a migration of existing projects (via ng update) to ignore it on git
  • it's surprising for users that files are created in their sources
  • it can remain on disk despite best efforts to remove it (e.g. process crash)
  • multiple targeted spec runs will interfere with each other because they use the same temp file

So for now we'd like to not add the temporary file at all. There are some ways of limiting the specs without it so we can still have targeted specs. But changing targeted specs mid-run without it would not be easy. So for now we propose to have targeted specs but not changing them on the fly, and later we could add a way to do that as well.

If we ignore the cross process communication we could easily avoid the temporary file by either

  • editing the tsconfig include for that run, causing only some specs to be compiled, or
  • creating a custom context for the require in test.ts, similar to what we do for lazy routes

I favor the first option myself because it also makes for a smaller, TS compilation, which should make both initial build and rebuilds faster.

I understand it's disappointing to be asked to reduce the scope of something you've designed and that covers your usecase very well. But for us as maintainers these questions of migration and usability are always key concerns.

For instance, anything that requires a migration can only be on a major since our policy is to only require migrations for majors. On the other hand something that requires no interaction with the user source is much easier to land. Things that have any chance of leaving artifacts in the user sources are also scrutinized because they are hard to clean after.

Please let me know what you think.

@FDIM
Copy link
Contributor Author

FDIM commented Feb 8, 2019

@filipesilva I agree 100% that generated/temp file is not ideal, but I do not know how else scope could be limited effectively without a way to feed webpack/typescript a stream/string instead of an entry point file.

I'll test if your theory holds by changing tsconfig include option and get back to you. Do you have any pointers for custom context? I'd like to see how that works as well. Anyway, I am fairly certain that require.context would scan full project structure on every build though.

On a flip side, I do not really agree with other team concerns:

  • requires a migration of existing projects (via ng update) to ignore it on git
  • it's surprising for users that files are created in their sources
  • it can remain on disk despite best efforts to remove it (e.g. process crash)

These steps can be fairly easily handled manually and documented as a quirk of --spec option for performance reasons - this is my number 1 reason for spending time on this! Not sure if you realized, but if you do not use the option - file will not be generated. I'd rather spend 5 minutes configuring my project manually and having fastest rebuild time than making this automatic process, but potentially slower (currently I have no idea if tsconfig include option would be slower/faster and if it would be a working solution) and having to restart the process to switch to a different test.
In other words, I am in favor of development experience/quick iteration when doing unit tests over configuration. Unfortunately some things do not have simple solutions.

  • multiple targeted spec runs will interfere with each other because they use the same temp file

I'd say this is more like a limitation that you can have only single targeted test run at the same time per project. Isn't it safe to assume that one would use this option when working on a specific feature/task?

Thanks for looking into this!

@filipesilva
Copy link
Contributor

This is where we manipulate the custom context for lazy routes:

compiler.hooks.contextModuleFactory.tap('angular-compiler', cmf => {
const angularCorePackagePath = require.resolve('@angular/core/package.json');
// APFv6 does not have single FESM anymore. Instead of verifying if we're pointing to
// FESMs, we resolve the `@angular/core` path and verify that the path for the
// module starts with it.
// This may be slower but it will be compatible with both APF5, 6 and potential future
// versions (until the dynamic import appears outside of core I suppose).
// We resolve any symbolic links in order to get the real path that would be used in webpack.
const angularCoreResourceRoot = fs.realpathSync(path.dirname(angularCorePackagePath));
cmf.hooks.afterResolve.tapPromise('angular-compiler', async result => {
// Alter only existing request from Angular or one of the additional lazy module resources.
const isLazyModuleResource = (resource: string) =>
resource.startsWith(angularCoreResourceRoot) ||
( this.options.additionalLazyModuleResources &&
this.options.additionalLazyModuleResources.includes(resource));
if (!result || !this.done || !isLazyModuleResource(result.resource)) {
return result;
}
return this.done.then(
() => {
// This folder does not exist, but we need to give webpack a resource.
// TODO: check if we can't just leave it as is (angularCoreModuleDir).
result.resource = path.join(this._basePath, '$$_lazy_route_resource');
// tslint:disable-next-line:no-any
result.dependencies.forEach((d: any) => d.critical = false);
// tslint:disable-next-line:no-any
result.resolveDependencies = (_fs: any, options: any, callback: Callback) => {
const dependencies = Object.keys(this._lazyRoutes)
.map((key) => {
const modulePath = this._lazyRoutes[key];
if (modulePath !== null) {
const name = key.split('#')[0];
return new this._contextElementDependencyConstructor(modulePath, name);
} else {
return null;
}
})
.filter(x => !!x);
if (this._options.nameLazyFiles) {
options.chunkName = '[request]';
}
callback(null, dependencies);
};
return result;
},
() => undefined,
);
});
});

These steps can be fairly easily handled manually and documented as a quirk of --spec option for performance reasons

Generally speaking, anything that requires a user to go figure out the quirks of the tool isn't great. And anything that requires a user to manually change their project isn't going to happen for the vast majority of projects.

Yes, the performance would be better. And I also understand that it would be much preferable for your usecase to have the "hot switch" feature. But I think that for most users, the base "targetted spec" feature gets them most of the way to faster test turnaround time without any quirks, pitfalls, needed upgrades, etc. Then the feature can be enhanced over time.

One thing I ask you to keep in mind is that we have a two release cycle deprecation policy. Since we release roughly every six months, that means that a new feature will need to be around and maintained for at least one year. I hope this puts into perspective why we favor less user-facing complexity and things that don't require upgrade paths.

@FDIM
Copy link
Contributor Author

FDIM commented Feb 11, 2019

You have very valid points from maintenance point of view and I agree, but this does not help in landing such feature to the 'core'. I am also a bit puzzled why there is no demand for such option (maybe not many enterprise apps have tests... I hope not!) or what people do after they pass certain number of tests (gonna have to check jest).

Anyway, I've tried tinkering and customizing tsconfig include option and from my understanding solution would not be simpler. Basically I'd have to recursively find all related files to targeted specs and pass them via files option or include option. I don't think there is a way to pass a mask to typescript and let it handle this automatically.

To make matters worse, consider this tsconfig:

{
    "extends": "./tsconfig.json",
    "exclude": [
        "./node_modules"
    ],
    "include": [
        "source/polyfills.ts",
        "source/test.ts",
        "source/common/components/input/**/*.ts"
    ]
}

One could expect that only files related to the listed ones would be included, but no - typescript also tries to compile files that are not referenced directly or indirectly (such as source/common/services/* or source/mw/*). Only changing require.context('./', true, /\.spec\..s$/); to require.context('./', true, /common\/components\/input\/.*\.spec\..s$/); in test.ts fixes the issue.

Could you share the solution that worked for you when you discussed include option as a potential alternative?

In the meantime I'll investigate what custom context does and how it works - thanks for that!

@filipesilva
Copy link
Contributor

filipesilva commented Feb 11, 2019

The tsconfig you showed is quite different from the ones we generate for specs, mind you. We explicitly set both files and include. If you don't set files, everything is included by default.

Tried my hand at manipulating the tsconfig manually:

  • made a new app with ng new targeted-specs-tsconfig
  • added a failing spec src/app/failing.spec.ts: it('should fail', () => expect(false).toBeTruthy());
  • npm run test -- --watch=false --browsers=ChromeHeadless fails
  • edit tsconfig to have this include property:
  "include": [
    "**/app.component.spec.ts",
    "**/*.d.ts"
  ]
  • npm run test -- --watch=false --browsers=ChromeHeadless fails with a badly formatted message that says
Uncaught Error: Module build failed (from ./node_modules/@ngtools/webpack/src/index.js):
Error: D:\\sandbox\\targeted-specs-tsconfig\\src\\app\\failing.spec.ts is missing from the TypeScript compilation. Please make sure it is in your tsconfig via the 'files' or 'include' property.

I expected that to work. But it doesn't because our webpack plugin errors out when requested TS files that aren't in the compilation.

We added this error specifically to avoid implicitly importing TS files. Maybe this is a restriction that we can relax on this particular case, and serve an empty file instead. But either way this approach got more complicated.

@filipesilva
Copy link
Contributor

filipesilva commented Feb 11, 2019

If you want to check how ignoring that error looks like, open node_modules/@ngtools/webpack/src/angular_compiler_plugin.js and comment out this block:

if (((fileName.endsWith('.ts') || fileName.endsWith('.tsx'))
&& !this._compilerHost.fileExists(fileName))
|| !this._compilerHost.fileExists(outputFile, false)) {
let msg = `${fileName} is missing from the TypeScript compilation. `
+ `Please make sure it is in your tsconfig via the 'files' or 'include' property.`;
if (/(\\|\/)node_modules(\\|\/)/.test(fileName)) {
msg += '\nThe missing file seems to be part of a third party library. '
+ 'TS files in published libraries are often a sign of a badly packaged library. '
+ 'Please open an issue in the library repository to alert its author and ask them '
+ 'to package the library using the Angular Package Format (https://goo.gl/jB3GVv).';
}
throw new Error(msg);
}

Doing that will work with my previous example and only run src/app/app.component.spec.ts.

I'm unsure of what the best API within the plugin for this would be, but probably an option that alters the include array, and ignores the error above if the file matches the alteration.

@FDIM FDIM force-pushed the feature/running-selected-specs branch from e8943f3 to 6d064b8 Compare May 21, 2019 21:27
@FDIM
Copy link
Contributor Author

FDIM commented May 21, 2019

@filipesilva I think I found a perfect solution!

I'll make a webpack loader (see last commit) that will be registered in karma builder for test.ts files which will transform it in the same way as in original suggestion.

I tested the idea in angular 8 project by modifying karma js file manually in node_modules and it seemed to work.

Now I have to figure out again how to work on cli locally, finalize formatter and update previous test. Hopefully I'll wrap this up this week.

EDIT: I also looked into your previous suggestion with tsconfig and files list, sadly this approach would not prevent entire project from being build as require.context() would still include all the spec files and potentially make performance gains negligible.

@FDIM FDIM force-pushed the feature/running-selected-specs branch from 6d064b8 to 9337f19 Compare May 22, 2019 19:52
@FDIM
Copy link
Contributor Author

FDIM commented May 22, 2019

I think I have it working! Only missing 2 things now:

  • support directory when one is passed as spec
  • figure out sourceRoot from target (so it would also work in multi-project setup)

@FDIM FDIM force-pushed the feature/running-selected-specs branch 2 times, most recently from 49b98c1 to 75f0a2b Compare May 23, 2019 20:57
@FDIM FDIM changed the title feat(@angular/cli): option to build and test only specified spec files feat(@angular-devkit/build-angular): option to build and test only specified spec files May 23, 2019
@FDIM FDIM force-pushed the feature/running-selected-specs branch from 75f0a2b to 90f3317 Compare May 23, 2019 21:19
@FDIM
Copy link
Contributor Author

FDIM commented May 23, 2019

PR is ready for review. @filipesilva would you mind having a look at current implementation and provide some feedback? Thanks!

Aside of that some help regarding failing circleci test would be great. Is it supposed to start actual chrome and run dummy tests? The same tests are passing locally and the feature is working as intended when build_angular package is installed in another project.

EDIT: nevermind about the failing test, I've changed suffix to spec_large.ts. Hopefully it will be green now

@FDIM FDIM force-pushed the feature/running-selected-specs branch from 90f3317 to 2c2fa09 Compare May 23, 2019 21:43
@FDIM FDIM closed this May 30, 2019
@FDIM FDIM force-pushed the feature/running-selected-specs branch from 84ffdf0 to 5df02a3 Compare May 30, 2019 11:29
@FDIM FDIM reopened this May 30, 2019
@FDIM FDIM force-pushed the feature/running-selected-specs branch 2 times, most recently from 998a5eb to 1cc47d7 Compare May 30, 2019 12:01
@FDIM
Copy link
Contributor Author

FDIM commented May 30, 2019

PR should be ready now. I've rebased from master and updated karma/schema.json with more details about spec option.

@FDIM FDIM force-pushed the feature/running-selected-specs branch from 1cc47d7 to eaea7f5 Compare May 30, 2019 12:29
@FDIM
Copy link
Contributor Author

FDIM commented May 30, 2019

I just had to adjust the text in schema.json, apparently having **/* in description results in broken schema.ts file. As it ends multi-line comment.

@FDIM FDIM force-pushed the feature/running-selected-specs branch from eaea7f5 to b479e04 Compare May 30, 2019 13:20
Copy link
Contributor

@filipesilva filipesilva left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it looks great! LGTM from my side, just waiting for @clydin to also take a look atm.

@@ -95,6 +112,31 @@ export function execute(
}
}

// prepend special webpack loader that will transform test.ts
if (webpackConfig && webpackConfig.module && options.spec) {
const sourceRoot = workspace.getProject(projectName).sourceRoot || '';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is sourceRoot used here?
The current behavior with the require.context call is that the files are relative to the test.ts (mainFilePath in this case).
However, I think it would be more intuitive if the option's base directory was the workspace root as this would be consist with all other options. A project might also not have the concept of a source root (its currently really an extension option for the browser builder).
This would also eliminate the need for all the extra code to find the source root.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, option's base directory is workspace root - path relative to source root is an alternative that is exposed, but mainly needed internally. sourceRoot is simply subtracted from what is passed as spec, so that typescript would be able to find these files in test.ts (main) as path relative to it is needed (see findSpecs).

Any suggestions on how to refactor this to not rely on sourceRoot?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The glob.sync call's cwd option can be the workspace root and the absolute option would also be needed. From there, get the absolute path of the directory name of the main file; and then loop and convert the test file list from the glob call to relative paths from the main file.

Copy link
Contributor Author

@FDIM FDIM Jun 21, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've ended up with a bit different solution and sourceRoot is no longer used. I also reverted some of the other changes. I hope it's OK now.

@filipesilva filipesilva added this to the 8.1.0 milestone Jun 18, 2019
@FDIM
Copy link
Contributor Author

FDIM commented Jun 19, 2019

@filipesilva I see you've added 8.1 milestone, this is great news!
I am just a bit confused on what should I change to finalize this? Would it be enough to rename option from spec to include (or files)?

@FDIM FDIM force-pushed the feature/running-selected-specs branch 2 times, most recently from 89ec427 to c41b1f8 Compare June 21, 2019 18:50
@FDIM FDIM force-pushed the feature/running-selected-specs branch from c41b1f8 to 905a6ce Compare June 21, 2019 18:56
@FDIM
Copy link
Contributor Author

FDIM commented Jun 21, 2019

I've rebased from master and I think this is ready to go. include option can now be passed multiple times and appropriate specs are executed in test project.
It's just a bit strange that I see a warning that the option is passed multiple times. Can one of you point out how one can pass an array to cli? e.g. codeCoverageExclude, there is no example in docs :(

@vikerman vikerman merged commit af2b1b5 into angular:master Jun 25, 2019
@filipesilva
Copy link
Contributor

@FDIM I updated your original PR message to list the new --include name, and remove references to --spec-update. I was pointing people here and they were confused that the spec option wasn't listed in docs.

@krokofant
Copy link

krokofant commented Jul 25, 2019

Angular CLI: 8.1.2
Node: 10.16.0
OS: win32 x64
Angular: 8.1.2
... animations, cli, common, compiler, compiler-cli, core, forms
... language-service, platform-browser, platform-browser-dynamic
... router, upgrade

Package                           Version
-----------------------------------------------------------
@angular-devkit/architect         0.800.6
@angular-devkit/build-angular     0.800.6
@angular-devkit/build-optimizer   0.800.6
@angular-devkit/build-webpack     0.800.6
@angular-devkit/core              7.3.4
@angular-devkit/schematics        8.1.2
@angular/cdk                      8.1.1
@ngtools/webpack                  8.0.6
@schematics/angular               8.1.2
@schematics/update                0.801.2
rxjs                              6.5.2
typescript                        3.4.5
webpack                           4.30.0
npx ng test myProject --include something
Unknown option: '--include'
Unknown option: 'something'

Shouldn't this work?

Omitting the project name produces double error messages:

Unknown option: '--include'
Unknown option: 'something'
Unknown option: '--include'
Unknown option: 'something'

Edit: Started to work after i upgraded the @Angular-devkit packages

@filipesilva
Copy link
Contributor

@krokofant yeah you need to update @angular-devkit/build-angular to include these changes.

@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Sep 13, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
target: major This PR is targeted for the next major release
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Run A Single Unit Test
9 participants