1 module served.commands.test_provider;
2 
3 import served.types;
4 import served.commands.symbol_search;
5 import served.utils.async : spawnFiber;
6 
7 import workspaced.api;
8 import workspaced.coms;
9 
10 import std.algorithm;
11 import std.experimental.logger;
12 import std.file : FileException;
13 import std.path : baseName;
14 import std.range;
15 
16 /// Set to true by app.d if `--provide test-runner` is given
17 /// Makes serve-d emit unittests on every save
18 __gshared bool doTrackTests = false;
19 
20 UnittestProject[DocumentUri] projectTests;
21 
22 @onProjectAvailable
23 void onPreviewProjectForUnittest(WorkspaceD.Instance instance, string dir, string rootFolderUri)
24 {
25 	if (!doTrackTests)
26 		return;
27 
28 	string rootUri = instance.cwd.uriFromFile;
29 	auto project = &projectTests.require(rootUri, UnittestProject(rootUri, null, null, true));
30 
31 	rpc.notifyMethod("coded/pushProjectTests", *project);
32 }
33 
34 @onAddedProject
35 void onDidAddProjectForUnittest(WorkspaceD.Instance instance, string dir, string rootFolderUri)
36 {
37 	if (!doTrackTests)
38 		return;
39 
40 	spawnFiber({
41 		if (!instance.has!DubComponent)
42 			return;
43 
44 		rescanProject(instance);
45 	}, requiredLibdparsePageCount * 2);
46 }
47 
48 @protocolNotification("textDocument/didSave")
49 void onDidSaveCheckForUnittest(DidSaveTextDocumentParams params)
50 {
51 	if (!doTrackTests)
52 		return;
53 
54 	if (!params.textDocument.uri.endsWith(".d"))
55 		return;
56 
57 	auto instance = backend.getBestInstance!DubComponent(params.textDocument.uri);
58 	if (!instance)
59 		return;
60 
61 	spawnFiber({
62 		auto projectUri = workspace(params.textDocument.uri).folder.uri;
63 
64 		auto project = &projectTests.require(projectUri, UnittestProject(projectUri));
65 		rescanFile(*project, params.textDocument);
66 
67 		rpc.notifyMethod("coded/pushProjectTests", *project);
68 	}, requiredLibdparsePageCount * 2);
69 }
70 
71 @protocolNotification("served/rescanTests")
72 void rescanTests(RescanTestsParams params)
73 {
74 	string cwd = params.uri.length ? uriToFile(params.uri) : null;
75 	foreach (instance; backend.instances)
76 	{
77 		if (!cwd.length || instance.cwd == cwd)
78 		{
79 			if (auto lazyInstance = cast(LazyWorkspaceD.LazyInstance)instance)
80 			{
81 				if (cwd.length || lazyInstance.didCallAccess)
82 				{
83 					rescanProject(instance);
84 				}
85 			}
86 			else
87 			{
88 				rescanProject(instance);
89 			}
90 		}
91 	}
92 }
93 
94 private void rescanFile(ref UnittestProject project, TextDocumentIdentifier documentIdentifier)
95 {
96 	auto document = documents[documentIdentifier.uri];
97 	auto symbols = provideDocumentSymbolsOld(DocumentSymbolParamsEx(documentIdentifier, true));
98 
99 	UnittestInfo[] tests;
100 
101 	foreach (test; symbols)
102 	{
103 		if (test.extendedType != SymbolKindEx.test)
104 			continue;
105 
106 		string name = test.name;
107 		if (test.detail.length)
108 			name = test.detail;
109 
110 		tests ~= UnittestInfo(test.name, name, test.containerName, test.location.range);
111 	}
112 
113 	string modulename = backend.get!ModulemanComponent.moduleName(document.rawText);
114 	if (!modulename.length)
115 		modulename = "(file) " ~ documentIdentifier.uri.uriToFile.baseName;
116 	auto entry = UnittestModule(modulename, documentIdentifier.uri, tests);
117 
118 	auto trisect = project.modules
119 		.assumeSorted!("a.moduleName < b.moduleName")
120 		.trisect(entry);
121 
122 	if (trisect[1].length)
123 	{
124 		if (tests.length == 0) // remove
125 			project.modules = project.modules.remove(tests.length);
126 		else
127 			project.modules[trisect[0].length] = entry;
128 	}
129 	else if (tests.length)
130 	{
131 		project.modules = project.modules[0 .. trisect[0].length]
132 			~ entry
133 			~ project.modules[trisect[0].length .. $];
134 	}
135 }
136 
137 private void rescanProject(WorkspaceD.Instance instance)
138 {
139 	import std.datetime.stopwatch : StopWatch;
140 	import std.path : buildNormalizedPath;
141 
142 	DubComponent dub = instance.get!DubComponent;
143 	auto settings = dub.rootPackageBuildSettings();
144 
145 	string rootUri = instance.cwd.uriFromFile;
146 
147 	auto project = &projectTests.require(rootUri, UnittestProject(rootUri));
148 
149 	StopWatch sw;
150 	sw.start();
151 
152 	project.name = settings.packageName;
153 	project.needsLoad = false;
154 	project.modules = null;
155 	foreach (path; settings.sourceFiles)
156 	{
157 		auto fullPath = buildNormalizedPath(settings.packagePath, path);
158 		if (!fullPath.endsWith(".d", ".dpp", ".di"))
159 			continue;
160 
161 		auto uri = fullPath.uriFromFile;
162 
163 		bool tempLoad;
164 		try
165 			documents.getOrFromFilesystem(uri, tempLoad);
166 		catch (FileException e)
167 		{
168 			warningf("Failed to read file %s for tests: %s", fullPath, e);
169 			continue;
170 		}
171 
172 		scope (exit)
173 			if (tempLoad)
174 				documents.unloadDocument(uri);
175 
176 		try
177 		{
178 			rescanFile(*project, TextDocumentIdentifier(uri));
179 		}
180 		catch (Exception e)
181 		{
182 			warningf("Failed to analyze file %s for tests: %s", fullPath, e);
183 		}
184 	}
185 
186 	rpc.notifyMethod("coded/pushProjectTests", *project);
187 
188 	sw.stop();
189 	infof("Found %s modules with tests in %s (%s) in %s",
190 		project.modules.length,
191 		project.name,
192 		project.workspaceUri,
193 		sw.peek);
194 }
195