Skip to content

Commit e3dcead

Browse files
committed
Fix the regression of the Flutter coverage runner
This resolves #7810
1 parent 162c8ec commit e3dcead

File tree

9 files changed

+632
-0
lines changed

9 files changed

+632
-0
lines changed
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright 2021 The Chromium Authors. All rights reserved.
3+
* Use of this source code is governed by a BSD-style license that can be
4+
* found in the LICENSE file.
5+
*/
6+
package io.flutter.run.coverage;
7+
8+
import com.intellij.coverage.CoverageDataManager;
9+
import com.intellij.coverage.CoverageSuitesBundle;
10+
import com.intellij.coverage.SimpleCoverageAnnotator;
11+
import com.intellij.openapi.module.Module;
12+
import com.intellij.openapi.project.Project;
13+
import com.intellij.openapi.roots.ModuleRootManager;
14+
import com.intellij.openapi.vfs.VirtualFile;
15+
import io.flutter.utils.FlutterModuleUtils;
16+
import org.jetbrains.annotations.NotNull;
17+
import org.jetbrains.annotations.Nullable;
18+
19+
import java.io.File;
20+
import java.util.ArrayList;
21+
import java.util.Arrays;
22+
import java.util.List;
23+
24+
public class FlutterCoverageAnnotator extends SimpleCoverageAnnotator {
25+
26+
@Nullable
27+
public static FlutterCoverageAnnotator getInstance(Project project) {
28+
return project.getService(FlutterCoverageAnnotator.class);
29+
}
30+
31+
public FlutterCoverageAnnotator(Project project) {
32+
super(project);
33+
}
34+
35+
@Override
36+
protected FileCoverageInfo fillInfoForUncoveredFile(@NotNull File file) {
37+
return new FileCoverageInfo();
38+
}
39+
40+
@Override
41+
protected boolean shouldCollectCoverageInsideLibraryDirs() {
42+
return false;
43+
}
44+
45+
@Override
46+
protected VirtualFile[] getRoots(Project project,
47+
@NotNull CoverageDataManager dataManager,
48+
CoverageSuitesBundle suite) {
49+
return dataManager.doInReadActionIfProjectOpen(() -> {
50+
final List<VirtualFile> roots = new ArrayList<>();
51+
for (Module module : FlutterModuleUtils.findModulesWithFlutterContents(project)) {
52+
final ModuleRootManager rootManager = ModuleRootManager.getInstance(module);
53+
roots.addAll(Arrays.asList(rootManager.getContentRoots()));
54+
}
55+
return roots.toArray(VirtualFile.EMPTY_ARRAY);
56+
});
57+
}
58+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright 2021 The Chromium Authors. All rights reserved.
3+
* Use of this source code is governed by a BSD-style license that can be
4+
* found in the LICENSE file.
5+
*/
6+
package io.flutter.run.coverage;
7+
8+
import com.intellij.coverage.CoverageDataManager;
9+
import com.intellij.coverage.CoverageRunner;
10+
import com.intellij.execution.configurations.RunConfigurationBase;
11+
import com.intellij.execution.configurations.coverage.CoverageEnabledConfiguration;
12+
import com.intellij.openapi.application.ModalityState;
13+
import com.intellij.openapi.diagnostic.Logger;
14+
import com.intellij.openapi.vfs.VirtualFile;
15+
import com.intellij.util.ModalityUiUtil;
16+
import io.flutter.pub.PubRoot;
17+
import io.flutter.run.test.TestConfig;
18+
import org.jetbrains.annotations.NotNull;
19+
import org.jetbrains.annotations.Nullable;
20+
21+
public class FlutterCoverageEnabledConfiguration extends CoverageEnabledConfiguration {
22+
private static final Logger LOG = Logger.getInstance(FlutterCoverageEnabledConfiguration.class.getName());
23+
24+
public FlutterCoverageEnabledConfiguration(@NotNull RunConfigurationBase<?> configuration) {
25+
super(configuration);
26+
super.setCoverageRunner(CoverageRunner.getInstance(FlutterCoverageRunner.class));
27+
createCoverageFile();
28+
ModalityUiUtil.invokeLaterIfNeeded(
29+
ModalityState.any(),
30+
() -> setCurrentCoverageSuite(CoverageDataManager.getInstance(configuration.getProject()).addCoverageSuite(this)));
31+
}
32+
33+
@Override
34+
protected String createCoverageFile() {
35+
if (myCoverageFilePath == null) {
36+
if (!(getConfiguration() instanceof TestConfig)) {
37+
return "";
38+
}
39+
VirtualFile file = ((TestConfig)getConfiguration()).getFields().getFileOrDir();
40+
final VirtualFile root = PubRoot.forFile(file).getRoot();
41+
myCoverageFilePath = root.getPath() + "/coverage/lcov.info";
42+
}
43+
return myCoverageFilePath;
44+
}
45+
46+
@Override
47+
public void setCoverageRunner(@Nullable final CoverageRunner coverageRunner) {
48+
// Save and restore myCoverageFilePath because the super method clears it.
49+
final String path = myCoverageFilePath;
50+
super.setCoverageRunner(coverageRunner);
51+
myCoverageFilePath = path;
52+
}
53+
54+
@Override
55+
public void coverageRunnerExtensionRemoved(@NotNull CoverageRunner runner) {
56+
final String path = myCoverageFilePath;
57+
super.coverageRunnerExtensionRemoved(runner);
58+
myCoverageFilePath = path;
59+
}
60+
}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
/*
2+
* Copyright 2021 The Chromium Authors. All rights reserved.
3+
* Use of this source code is governed by a BSD-style license that can be
4+
* found in the LICENSE file.
5+
*/
6+
package io.flutter.run.coverage;
7+
8+
import com.intellij.coverage.CoverageAnnotator;
9+
import com.intellij.coverage.CoverageEngine;
10+
import com.intellij.coverage.CoverageFileProvider;
11+
import com.intellij.coverage.CoverageRunner;
12+
import com.intellij.coverage.CoverageSuite;
13+
import com.intellij.coverage.CoverageSuitesBundle;
14+
import com.intellij.execution.configurations.RunConfigurationBase;
15+
import com.intellij.execution.configurations.RunProfile;
16+
import com.intellij.execution.configurations.WrappingRunConfiguration;
17+
import com.intellij.execution.configurations.coverage.CoverageEnabledConfiguration;
18+
import com.intellij.execution.testframework.AbstractTestProxy;
19+
import com.intellij.openapi.module.Module;
20+
import com.intellij.openapi.project.Project;
21+
import com.intellij.openapi.vfs.VirtualFile;
22+
import com.intellij.psi.PsiElement;
23+
import com.intellij.psi.PsiFile;
24+
import com.jetbrains.lang.dart.DartFileType;
25+
import com.jetbrains.lang.dart.psi.DartFile;
26+
import io.flutter.FlutterBundle;
27+
import io.flutter.FlutterUtils;
28+
import io.flutter.pub.PubRoot;
29+
import io.flutter.run.test.TestConfig;
30+
31+
import java.io.File;
32+
import java.util.HashSet;
33+
import java.util.List;
34+
import java.util.Set;
35+
36+
import org.jetbrains.annotations.NotNull;
37+
import org.jetbrains.annotations.Nullable;
38+
39+
public class FlutterCoverageEngine extends CoverageEngine {
40+
41+
public static FlutterCoverageEngine getInstance() {
42+
return CoverageEngine.EP_NAME.findExtensionOrFail(FlutterCoverageEngine.class);
43+
}
44+
45+
@Override
46+
public boolean isApplicableTo(@NotNull RunConfigurationBase conf) {
47+
return unwrapRunProfile(conf) instanceof TestConfig;
48+
}
49+
50+
@Override
51+
public boolean canHavePerTestCoverage(@NotNull RunConfigurationBase conf) {
52+
return true;
53+
}
54+
55+
@Override
56+
public @NotNull CoverageEnabledConfiguration createCoverageEnabledConfiguration(@NotNull RunConfigurationBase conf) {
57+
return new FlutterCoverageEnabledConfiguration(conf);
58+
}
59+
60+
@Override
61+
public @Nullable CoverageSuite createCoverageSuite(@NotNull CoverageRunner covRunner,
62+
@NotNull String name,
63+
@NotNull CoverageFileProvider coverageDataFileProvider,
64+
@Nullable String[] filters,
65+
long lastCoverageTimeStamp,
66+
@Nullable String suiteToMerge,
67+
boolean coverageByTestEnabled,
68+
boolean tracingEnabled,
69+
boolean trackTestFolders,
70+
Project project) {
71+
return null;
72+
}
73+
74+
@Override
75+
public @Nullable CoverageSuite createCoverageSuite(@NotNull CoverageRunner covRunner,
76+
@NotNull String name,
77+
@NotNull CoverageFileProvider coverageDataFileProvider,
78+
@NotNull CoverageEnabledConfiguration config) {
79+
if (config instanceof FlutterCoverageEnabledConfiguration) {
80+
return new FlutterCoverageSuite(covRunner, name, coverageDataFileProvider,
81+
config.getConfiguration().getProject(), this);
82+
}
83+
return null;
84+
}
85+
86+
@Override
87+
public @Nullable CoverageSuite createEmptyCoverageSuite(@NotNull CoverageRunner coverageRunner) {
88+
return new FlutterCoverageSuite(this);
89+
}
90+
91+
@Override
92+
public @NotNull CoverageAnnotator getCoverageAnnotator(Project project) {
93+
return FlutterCoverageAnnotator.getInstance(project);
94+
}
95+
96+
@Override
97+
public boolean coverageEditorHighlightingApplicableTo(@NotNull PsiFile psiFile) {
98+
final PubRoot root = PubRoot.forPsiFile(psiFile);
99+
if (root == null) return false;
100+
final VirtualFile file = psiFile.getVirtualFile();
101+
if (file == null) return false;
102+
final String path = root.getRelativePath(file);
103+
if (path == null) return false;
104+
return path.startsWith("lib") && FlutterUtils.isDartFile(file);
105+
}
106+
107+
@Override
108+
public boolean coverageProjectViewStatisticsApplicableTo(VirtualFile fileOrDir) {
109+
return !fileOrDir.isDirectory() && fileOrDir.getFileType() instanceof DartFileType;
110+
}
111+
112+
@Override
113+
public boolean acceptedByFilters(@NotNull PsiFile psiFile, @NotNull CoverageSuitesBundle suite) {
114+
return psiFile instanceof DartFile;
115+
}
116+
117+
@Override
118+
public boolean recompileProjectAndRerunAction(@NotNull Module module,
119+
@NotNull CoverageSuitesBundle suite,
120+
@NotNull Runnable chooseSuiteAction) {
121+
return false;
122+
}
123+
124+
@Override
125+
public String getQualifiedName(@NotNull final File outputFile,
126+
@NotNull final PsiFile sourceFile) {
127+
return getQName(sourceFile);
128+
}
129+
130+
@Override
131+
public @NotNull Set<String> getQualifiedNames(@NotNull PsiFile sourceFile) {
132+
final Set<String> qualifiedNames = new HashSet<>();
133+
qualifiedNames.add(getQName(sourceFile));
134+
return qualifiedNames;
135+
}
136+
137+
@Override
138+
public List<PsiElement> findTestsByNames(@NotNull String[] testNames, @NotNull Project project) {
139+
return null;
140+
}
141+
142+
@Override
143+
public @Nullable String getTestMethodName(@NotNull PsiElement element, @NotNull AbstractTestProxy testProxy) {
144+
return null;
145+
}
146+
147+
@Override
148+
public String getPresentableText() {
149+
return FlutterBundle.message("flutter.coverage.presentable.text");
150+
}
151+
152+
@NotNull
153+
private static String getQName(@NotNull PsiFile sourceFile) {
154+
return sourceFile.getVirtualFile().getPath();
155+
}
156+
157+
static @NotNull RunProfile unwrapRunProfile(@NotNull RunProfile runProfile) {
158+
if (runProfile instanceof WrappingRunConfiguration) {
159+
return ((WrappingRunConfiguration<?>)runProfile).getPeer();
160+
}
161+
return runProfile;
162+
}
163+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*
2+
* Copyright 2021 The Chromium Authors. All rights reserved.
3+
* Use of this source code is governed by a BSD-style license that can be
4+
* found in the LICENSE file.
5+
*/
6+
package io.flutter.run.coverage;
7+
8+
import com.intellij.coverage.CoverageDataManager;
9+
import com.intellij.coverage.CoverageExecutor;
10+
import com.intellij.coverage.CoverageRunnerData;
11+
import com.intellij.execution.ExecutionException;
12+
import com.intellij.execution.configurations.ConfigurationInfoProvider;
13+
import com.intellij.execution.configurations.RunProfile;
14+
import com.intellij.execution.configurations.RunProfileState;
15+
import com.intellij.execution.configurations.RunnerSettings;
16+
import com.intellij.execution.configurations.coverage.CoverageEnabledConfiguration;
17+
import com.intellij.execution.process.ProcessAdapter;
18+
import com.intellij.execution.process.ProcessEvent;
19+
import com.intellij.execution.process.ProcessHandler;
20+
import com.intellij.execution.runners.DefaultProgramRunnerKt;
21+
import com.intellij.execution.runners.ExecutionEnvironment;
22+
import com.intellij.execution.runners.GenericProgramRunner;
23+
import com.intellij.execution.ui.RunContentDescriptor;
24+
import com.intellij.openapi.application.ApplicationManager;
25+
import com.intellij.openapi.diagnostic.Logger;
26+
import com.intellij.openapi.vfs.LocalFileSystem;
27+
import com.intellij.openapi.vfs.VfsUtil;
28+
import io.flutter.FlutterBundle;
29+
import io.flutter.run.test.TestConfig;
30+
import org.jetbrains.annotations.NonNls;
31+
import org.jetbrains.annotations.NotNull;
32+
import org.jetbrains.annotations.Nullable;
33+
34+
import java.nio.file.Files;
35+
import java.nio.file.Path;
36+
import java.nio.file.Paths;
37+
38+
public class FlutterCoverageProgramRunner extends GenericProgramRunner<RunnerSettings> {
39+
private static final Logger LOG = Logger.getInstance(FlutterCoverageProgramRunner.class.getName());
40+
41+
private static final String ID = "FlutterCoverageProgramRunner";
42+
private ProcessHandler handler;
43+
private ProcessAdapter listener;
44+
45+
@Override
46+
public @NotNull
47+
@NonNls
48+
String getRunnerId() {
49+
return ID;
50+
}
51+
52+
@Override
53+
public boolean canRun(@NotNull String executorId, @NotNull RunProfile profile) {
54+
return executorId.equals(CoverageExecutor.EXECUTOR_ID) && profile instanceof TestConfig;
55+
}
56+
57+
@Override
58+
public RunnerSettings createConfigurationData(@NotNull final ConfigurationInfoProvider settingsProvider) {
59+
return new CoverageRunnerData();
60+
}
61+
62+
@Override
63+
@Nullable
64+
protected RunContentDescriptor doExecute(final @NotNull RunProfileState state,
65+
final @NotNull ExecutionEnvironment env) throws ExecutionException {
66+
final RunContentDescriptor result = DefaultProgramRunnerKt.executeState(state, env, this);
67+
if (result == null) {
68+
return null;
69+
}
70+
handler = result.getProcessHandler();
71+
if (handler != null) {
72+
listener = new ProcessAdapter() {
73+
@Override
74+
public void processTerminated(@NotNull ProcessEvent event) {
75+
ApplicationManager.getApplication().invokeLater(() -> processCoverage(env));
76+
}
77+
};
78+
handler.addProcessListener(listener);
79+
}
80+
return result;
81+
}
82+
83+
private void processCoverage(ExecutionEnvironment env) {
84+
if (!(env.getRunProfile() instanceof TestConfig runConfig)) return;
85+
final CoverageEnabledConfiguration configuration = CoverageEnabledConfiguration.getOrCreate(runConfig);
86+
if (configuration.getCoverageFilePath() == null) return;
87+
88+
final Path path = Paths.get(configuration.getCoverageFilePath());
89+
final Path cov = path.getParent();
90+
VfsUtil.markDirtyAndRefresh(false, false, true, LocalFileSystem.getInstance().findFileByPath(cov.getParent().toString()));
91+
VfsUtil.markDirtyAndRefresh(false, true, true, LocalFileSystem.getInstance().findFileByPath(cov.toString()));
92+
if (Files.exists(path)) {
93+
@Nullable final RunnerSettings settings = env.getRunnerSettings();
94+
if (settings != null) {
95+
CoverageDataManager.getInstance(env.getProject()).processGatheredCoverage(runConfig, settings);
96+
handler.removeProcessListener(listener);
97+
handler = null;
98+
listener = null;
99+
}
100+
}
101+
else {
102+
LOG.error(FlutterBundle.message("coverage.path.not.found", path));
103+
}
104+
}
105+
}

0 commit comments

Comments
 (0)