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