238: Resolve the install tasks within their own project - #332
DanielJette wants to merge 4 commits into
Conversation
AndroidTestifyBot
left a comment
There was a problem hiding this comment.
Changes requested
The message is well written, but this turns a recoverable misconfiguration into a build that will not configure at all, and it does so instead of fixing a lookup that has a one-line fix.
1. The failure is thrown for every Gradle invocation, not just the screenshot tasks
setDependencies runs from afterEvaluate, so the throw happens at configuration time. I set moduleName "features:FlixLibrary" on :FlixLibrary and ran ./gradlew help:
| result | |
|---|---|
main |
BUILD SUCCESSFUL |
| this PR | A problem occurred configuring project ':FlixLibrary'. > Testify could not find the installDebugAndroidTest task |
That means assemble, unit tests and IDE sync all fail. The inference is wrong for every nested module (:feature:login is an ordinary layout, not just composite builds), so on upgrade each of those projects stops syncing until moduleName is set. The description says it "will surface as a new failure"; it should say the whole build stops configuring.
2. The message's own remediation cannot be run
Run
./gradlew features:FlixLibrary:testifySettingsto print the resolved configuration, and./gradlew tasks --allto find the task's real path.
Both commands configure the project, so both die with this same error. The advice is unusable in exactly the situation it is printed.
3. The lookup doesn't need moduleName at all
inferredInstallTask / inferredAndroidTestInstallTask read this.tasks.names — the names come from this project. Building ":$moduleName:$name" and calling findByPath to find them again is an indirection that can only fail. project.tasks.findByName(taskName) resolves in a top-level module, a nested module and an included build alike, with no dependence on moduleName.
With that change #238 is actually fixed rather than reported, and the only remaining way to get null for a non-null name is a user who explicitly set installTask / installAndroidTestTask to something that doesn't exist. Throwing there is right, and the message can be much shorter. The "deliberately not included" paragraph argues against changing the moduleName default; this doesn't touch that default.
4. Docs now contradict the code
docs/docs/recipes/23-build-types-and-flavors.md ("Nested modules") still says "the plugin skips installing without an error (#238)", and the new message links to that very section. docs/docs/errors.md has no entry for the new error.
Smaller points
- The KDoc and CHANGELOG say a
com.android.testmodule has no install task. On #333'sFlixTest,testifySettingsprintsinstallTask = installDebug. OnlyinstallAndroidTestTaskis null there. - "inferred from the project name" is detected with
moduleName == project.name, which is also true when someone set it explicitly to that value. - The tests mock
Project,TaskContainerandTestifySettings, so they pin the string, not the behaviour. Nothing covers a real nested module; a TestKit case alongsideConfigurationCacheTestwould.
Verified
Plugin:ktlintCheck Plugin:test Plugin:assemble pass locally (6/6 new tests, ConfigurationCacheTest green). Merged with #333, FlixTest:testifySettings still configures. CHANGELOG hunk conflicts with the other five open code PRs.
|
You were right on all four, and point 3 was the one that mattered — the PR has been reshaped around it. Retitled, since it is now a fix rather than a diagnostic. 3. The lookup doesn't need
|
AndroidTestifyBot
left a comment
There was a problem hiding this comment.
Changes requested
The core fix is right and verified. One part of the old problem survives in the remaining error path, and the regression test doesn't cover the regression.
Verified: #238 is fixed
With moduleName "features:FlixLibrary" on :FlixLibrary:
$ ./gradlew help BUILD SUCCESSFUL
$ ./gradlew FlixLibrary:screenshotTest --dry-run :FlixLibrary:installDebugAndroidTest SKIPPED
$ ./gradlew FlixLibrary:screenshotTest > Task :FlixLibrary:installDebugAndroidTest ... OK (3 tests)
findByName, with findByPath kept for a value containing :, is the right shape. The docs and CHANGELOG now describe what the code does. Plugin:ktlintCheck Plugin:test Plugin:assemble pass locally.
1. The remaining error's advice still can't be run
The comment says the new message points at "<module>:tasks --all — a command that works, because that project configures fine." It doesn't. The throw is still in afterEvaluate, so with installAndroidTestTask "installNopeDebugAndroidTest" on :FlixLibrary:
$ ./gradlew help
A problem occurred configuring project ':FlixLibrary'. BUILD FAILED
$ ./gradlew :FlixLibrary:tasks --all
A problem occurred configuring project ':FlixLibrary'. BUILD FAILED
The scope is now much smaller (only a value someone typed), and failing on that is reasonable. But the remedy the message gives dies on the same error, and so does every other command in the build, IDE sync included. Two ways out:
- Defer the failure to execution: register the dependency lazily, or fail in a
doFirstonscreenshotTest/screenshotRecord. Configuration then succeeds andtasks --allworks. - Or keep it at configuration and change the advice to something that can run: remove the setting first, then list the tasks.
errors.mdsays the same thing and needs the same correction.
2. InstallTaskDependencyTest would pass on main
All four cases use :FlixLibrary and :LegacySample. Both are top-level, moduleName == project.name, and the old findByPath(":$moduleName:…") resolves for both. So the class described as the thing that makes a dropped dependency visible doesn't exercise the case that dropped it. It needs one module whose moduleName isn't its path: a TestKit fixture with a nested :feature:ui, or a -P hook that sets moduleName on an existing module. The negative doesNotContain(":FlixLibrary:installDebug\n") also depends on Gradle's line formatting; anchoring on the SKIPPED suffix would be sturdier.
Minor
./gradlew $path:tasks --all prints ./gradlew ::tasks --all if the plugin is ever applied to the root project.
|
Both points taken, and the first one found a configuration-cache regression on the way. 1. The failure is now deferred to the taskYou were right that my previous comment was wrong about this: the throw was still in The first attempt broke the configuration cache. Putting
2. The test would have passed on
|
AndroidTestifyBot
left a comment
There was a problem hiding this comment.
Changes requested — one placement fix
screenshotTest is right now. screenshotRecord still runs the whole recording before reporting the misconfiguration.
Verified
With installAndroidTestTask "installNopeDebugAndroidTest" on :FlixLibrary:
$ ./gradlew help BUILD SUCCESSFUL
$ ./gradlew :FlixLibrary:tasks --all BUILD SUCCESSFUL
$ ./gradlew FlixLibrary:screenshotTest > Task :FlixLibrary:screenshotTest FAILED
> Testify could not find the task `installNopeDebugAndroidTest`, configured as `installAndroidTestTask` ...
- The configuration-cache fix (capturing the
String, not theProject) is right. - The init-script case finally exercises a
moduleNamethat isn't the project path. Plugin:ktlintCheck Plugin:test Plugin:assemblepass locally, includingConfigurationCacheTest.- The docs build is clean.
The screenshotRecord guard fires last
ScreenshotRecordTask.setDependencies passes the record task into ScreenshotTestTask.setDependencies, so the doFirst lands on screenshotRecord. That's the no-op placeholder, and it only runs after everything it depends on:
$ ./gradlew FlixLibrary:screenshotRecord
> Task :FlixLibrary:screenshotClear
> Task :FlixLibrary:screenshotTestRecord
OK (3 tests)
> Task :FlixLibrary:screenshotPull
> Task :FlixLibrary:screenshotRecord FAILED
> Testify could not find the task `installNopeDebugAndroidTest` ...
The device was cleared, the tests ran in record mode against whatever APK was already installed (the install dependency is exactly what's missing), and screenshotPull copied the results into src/androidTest/assets. Only then did the build fail. A user can be left with baselines recorded from a stale build, plus a failure message that doesn't mention it.
Attach the check to the first task that does real work instead: a doFirst on screenshotClear and on the internal screenshotTestRecord, or have screenshotRecord's path guard InternalScreenshotTestRecordTask rather than the placeholder. A dry-run assertion in InstallTaskDependencyTest that screenshotClear doesn't run when the setting is bad would pin it.
|
Agreed, and this was the dangerous one. The guard moved to the tasks that do the workExtracted
I also guarded The testA dry run doesn't execute task actions, so assertThat(result.output).contains(":FlixLibrary:screenshotClear FAILED")
assertThat(result.output).doesNotContain(":FlixLibrary:screenshotTestRecord")
assertThat(result.output).doesNotContain(":FlixLibrary:screenshotPull")They need a device, since the device-setting tasks run first, so they are gated with the same VerifiedCorrectly configured, the chain is unchanged — One tidy-up in passing: my previous commit left |
AndroidTestifyBot
left a comment
There was a problem hiding this comment.
Approved
The record chain is now guarded where the work happens. I verified it on an API 37 emulator.
With installAndroidTestTask "installNopeDebugAndroidTest" on :FlixLibrary:
$ ./gradlew FlixLibrary:screenshotRecord
> Task :FlixLibrary:screenshotClear FAILED
> Testify could not find the task `installNopeDebugAndroidTest`, configured as `installAndroidTestTask` ...
BUILD FAILED in 1s
$ git status --short Samples/Flix/FlixLibrary/src # empty
$ ./gradlew help BUILD SUCCESSFUL
screenshotTestRecord and screenshotPull never run, and no baselines are touched.
Correctly configured, the chain is unchanged:
installDebugAndroidTest → screenshotClear → screenshotTestRecord (OK (3 tests)) → screenshotPull → screenshotRecord
git status is clean afterwards. With --configuration-cache, the entry is stored on the first run and reused on the second, so capturing only the String holds up.
Plugin:ktlintCheck Plugin:test Plugin:assemble pass locally. All 7 InstallTaskDependencyTest cases pass, including the two new buildAndFail cases. Asserting on which task failed and what didn't run is the right test for this; a dry run couldn't have seen it. CI is green.
Non-blocking:
screenshotClearrun on its own now also fails on this misconfiguration, although it doesn't need an install task. That is the right trade for guarding the chain; just noting it's a visible change for anyone who runs it standalone.assumeDevice()matchesConnected devices = 1, so with two devices attached the two new cases skip silently rather than run. Matching "not 0" would be more forgiving. Also worth confirming the BitrisePluginstep has a device, or these two never run in CI.- This and #333 both edit
ScreenshotTestTask.setDependencies; whichever lands second needs a careful rebase.
What does this change accomplish?
Fixes #238
Applying the plugin to a nested module silently produces a build that never installs the APKs. The
reporter's case: for
feature-2:ui,moduleNamefalls back toproject.name—ui— so the pluginlooked for
:ui:installDebugAndroidTest, which does not exist. The result was discarded with?.let, so the tests ran against whatever was already on the device.This is not limited to composite builds.
:feature:loginis an ordinary nested layout and hits itthe same way. It is also the root cause behind several confused reports on #131.
How have you achieved it?
The lookup does not need
moduleNameat all.inferredInstallTaskandinferredAndroidTestInstallTaskpick the task name out ofproject.tasks.names— the names comefrom this project. Rebuilding
":$moduleName:$taskName"and resolving it withfindByPathwas anindirection that could only ever fail;
tasks.findByName(taskName)resolves in a top-level module, anested module and an included build alike.
So #238 is now fixed rather than reported. With the lookup independent of
moduleName, the only wayleft to have a name that does not resolve is an
installTask/installAndroidTestTaskthat you setyourself naming a task that does not exist. That is a genuine misconfiguration, it throws, and the
message is three lines. A configured value containing
:is treated as a task path, so naming a taskin another project still works.
This does not touch the
moduleNamedefault.project.pathcarries a leading colon and the oldlookup interpolated another, which would have produced
::feature:login:installDebugAndroidTest—the same class of bug as #195.
Scope of Impact and Testing instructions
A nested module that was silently skipping the install now installs. No new configuration-time
failure for anyone who has not explicitly configured a task name.
CHANGELOG entry rewritten to describe the fix rather than the diagnostic.
Docs.
recipes/23-build-types-and-flavors.mdsaid the plugin "skips installing without an error(#238)"; it now explains that
moduleNameno longer affects installation but still determines theGradle commands printed in failure messages, which is the real reason to set it on a nested module.
errors.md'sGradleExtensionExceptionentry covers the new message.Tests.
InstallTaskResolutionTestrewritten — 6 cases, including that the lookup never callsfindByPathfor a simple name, which is the #238 guard. All 6 fail againstmain's implementation.InstallTaskDependencyTestadded alongsideConfigurationCacheTest, because mockedTaskContainertests pin the lookup but not the wiring, and a dropped dependency is only visible ina graph. It runs
--dry-runagainst the real root project — no device needed — and asserts:FlixLibrary:screenshotTestand:screenshotRecordboth carryinstallDebugAndroidTest,:LegacySample:screenshotTestcarries both install tasks, and a library module has no plaininstallDebugto depend on.Verified locally:
The reviewer's repro, which is the point of the change: