1 /*
2 SPDX-FileCopyrightText: 2016 Anton Anikin <anton.anikin@htower.ru>
3
4 SPDX-License-Identifier: GPL-2.0-or-later
5 */
6
7 #include "test_kdevformatsource.h"
8 #include "../kdevformatfile.h"
9 #include "../filesystemhelpers.h"
10
11 #include <QTest>
12 #include <QByteArray>
13 #include <QByteArrayList>
14 #include <QDebug>
15 #include <QDir>
16 #include <QFile>
17 #include <QFileInfo>
18 #include <QString>
19 #include <QStringList>
20 #include <QTemporaryDir>
21 #include <QTextStream>
22 #include <QStandardPaths>
23
24 #include <vector>
25
26 QTEST_MAIN(KDevelop::TestKdevFormatSource)
27
28 using namespace KDevelop;
29
30 namespace {
applyFormatting(const QString & path,bool expectedFormattingResult)31 QString applyFormatting(const QString& path, bool expectedFormattingResult)
32 {
33 KDevFormatFile formatFile(path, path);
34 if (!formatFile.find()) {
35 return "found no format_sources file for " + path;
36 }
37 if (!formatFile.read()) {
38 return "reading format_sources file failed for " + path;
39 }
40 if (formatFile.apply() != expectedFormattingResult) {
41 if (expectedFormattingResult) {
42 return "formatting was expected to succeed but actually failed for " + path;
43 } else {
44 return "formatting was expected to fail but actually succeeded for " + path;
45 }
46 }
47 return QString{};
48 }
49 }
50
TestKdevFormatSource()51 TestKdevFormatSource::TestKdevFormatSource()
52 {
53 }
54
~TestKdevFormatSource()55 TestKdevFormatSource::~TestKdevFormatSource()
56 {
57 }
58
initTestCase()59 void TestKdevFormatSource::initTestCase()
60 {
61 QStandardPaths::setTestModeEnabled(true);
62 }
63
testNotFound_data()64 void TestKdevFormatSource::testNotFound_data()
65 {
66 static const QStringList formatFileData = {};
67
68 QCOMPARE(initTest(formatFileData), true);
69
70 for (const Source& source : qAsConst(m_sources)) {
71 QTest::newRow(source.path.toUtf8()) << source.path << false << false << false << source.lines;
72 }
73 }
74
testNotFound()75 void TestKdevFormatSource::testNotFound()
76 {
77 runTest();
78 }
79
testNoCommands_data()80 void TestKdevFormatSource::testNoCommands_data()
81 {
82 static const QStringList formatFileData = {QStringLiteral("# some comment")};
83
84 QCOMPARE(initTest(formatFileData), true);
85
86 for (const Source& source : qAsConst(m_sources)) {
87 QTest::newRow(source.path.toUtf8()) << source.path << true << false << false << source.lines;
88 }
89 }
90
testNoCommands()91 void TestKdevFormatSource::testNoCommands()
92 {
93 runTest();
94 }
95
testNotMatch_data()96 void TestKdevFormatSource::testNotMatch_data()
97 {
98 static const QStringList formatFileData = {QStringLiteral("notmatched.cpp : unused_command")};
99
100 QCOMPARE(initTest(formatFileData), true);
101
102 for (const Source& source : qAsConst(m_sources)) {
103 QTest::newRow(source.path.toUtf8()) << source.path << true << true << false << source.lines;
104 }
105 }
106
testNotMatch()107 void TestKdevFormatSource::testNotMatch()
108 {
109 runTest();
110 }
111
testMatch1_data()112 void TestKdevFormatSource::testMatch1_data()
113 {
114 static const QStringList formatFileData({
115 QStringLiteral("src1/source_1.cpp : cat $ORIGFILE | sed 's/foo/FOO/' > tmp && mv tmp $ORIGFILE"),
116 QStringLiteral("src2/source_2.cpp : cat $ORIGFILE | sed 's/sqrt/std::sqrt/' > tmp && mv tmp $ORIGFILE"),
117 QStringLiteral("*.cpp : cat $ORIGFILE | sed 's/z/Z/' > tmp && mv tmp $ORIGFILE"),
118 QStringLiteral("notmatched.cpp : unused_command"),
119 });
120
121 QCOMPARE(initTest(formatFileData), true);
122
123 m_sources[0].lines.replaceInStrings(QStringLiteral("foo"), QStringLiteral("FOO"));
124 m_sources[1].lines.replaceInStrings(QStringLiteral("sqrt"), QStringLiteral("std::sqrt"));
125 m_sources[2].lines.replaceInStrings(QStringLiteral("z"), QStringLiteral("Z"));
126
127 for (const Source& source : qAsConst(m_sources)) {
128 QTest::newRow(source.path.toUtf8()) << source.path << true << true << true << source.lines;
129 }
130 }
131
testMatch1()132 void TestKdevFormatSource::testMatch1()
133 {
134 runTest();
135 }
136
testMatch2_data()137 void TestKdevFormatSource::testMatch2_data()
138 {
139 static const QStringList formatFileData({QStringLiteral("cat $ORIGFILE | sed 's/;/;;/' > tmp && mv tmp $ORIGFILE")});
140
141 QCOMPARE(initTest(formatFileData), true);
142
143 for (Source& source : m_sources) {
144 source.lines.replaceInStrings(QStringLiteral(";"), QStringLiteral(";;"));
145 QTest::newRow(source.path.toUtf8()) << source.path << true << true << true << source.lines;
146 }
147 }
148
testMatch2()149 void TestKdevFormatSource::testMatch2()
150 {
151 runTest();
152 }
153
testWildcardPathMatching_data()154 void TestKdevFormatSource::testWildcardPathMatching_data()
155 {
156 struct FormatInfo{ const char* dir; const char* contents; };
157 struct Row{
158 const char* dataTag;
159 std::vector<FormatInfo> formatInfos;
160 std::vector<const char*> unmatchedPaths;
161 std::vector<const char*> matchedPaths;
162 };
163
164 const std::vector<Row> dataRows{
165 Row{"format_sources without wildcards (simple syntax)",
166 {FormatInfo{"", "true"}},
167 {},
168 {"x", "a/b", "exclude", "x.c", "p q\tr", "v/l/p/a/t/h.x"}
169 }, Row{"Single root format_sources with a single command",
170 {FormatInfo{"", "rd/* *include* *.h : true"}},
171 {"x", "r", "r.d", "includ", "includh", "rdh", "rd.h/x", "a/b.hh", "rc/x.h/y"},
172 {"x.h", "rd/x", "rd/x.h", "aincludeb", "include", "include.h", "rd/a/b/c", "a/b/c.h", "a/include"}
173 }, Row{"Single root format_sources with different commands",
174 {FormatInfo{"", "*inc/*:\n q/* *x?z:true \n dd/*: \n *.c:false \n *ab*:true"}},
175 {"q", "a.b", "xz", "xyzc", "c", "ac", "inc", "inc-/x", "ayz", "xy", "add/s", "incc", "a./c", "x/yz", "a-b", "minc"},
176 {"xyz", "x.c", "incxyz", "ainc/b.c", "a/b/.c", "a/.c", "x/z", "a/x-z", "p/x.z", "asinc/v", "a/b/cab/d/e", "dd/d", "dd/.c"}
177 }, Row{"Multiple format_sources files",
178 {FormatInfo{"a/b/", "q/* *x?z : false"}, FormatInfo{"", "*.c *cab* : true"}},
179 {"a/q", "a/xyz", "q/x", "xz", "a/b/qu", "a/bu/xyz", "ab/q/x", "a/b/qt/x", "a/bxyz", "a/x/z", "a/b/xz", "a/b/.c", "a/b/x-z.c"},
180 {"a/b/xyz", "x.c", "a/b/cdxyz", "a/b/cd/xyz", "a/b/q/x", "a/.c", "a/b/x/z", "exclude.c", "a/bcab/d/e"}
181 }, Row{"Case sensitivity",
182 {FormatInfo{"", "pQ* *RS* : true"}},
183 {"a/b/CDE", "cdpq", "a/b/.e", "a/b/cDe", "prcpQ.Eqs"},
184 {"a/b/pQrs", "a/b/c/d/pq/rs", "a/b/RSPQ", "pq", "uvrs", "PQa/b"}
185 }};
186
187 QTest::addColumn<QStringList>("formatDirs");
188 QTest::addColumn<QByteArrayList>("formatContents");
189 QTest::addColumn<QStringList>("unmatchedPaths");
190 QTest::addColumn<QStringList>("matchedPaths");
191
192 for (const Row& row : dataRows) {
193 QStringList formatDirs;
194 QByteArrayList formatContents;
195 for (const FormatInfo& info : row.formatInfos) {
196 formatDirs.push_back(info.dir);
197 formatContents.push_back(info.contents);
198 }
199 #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
200 const QStringList unmatchedPaths(row.unmatchedPaths.cbegin(), row.unmatchedPaths.cend());
201 const QStringList matchedPaths(row.matchedPaths.cbegin(), row.matchedPaths.cend());
202 #else
203 const auto vectorToList = [](const std::vector<const char*>& vec) {
204 QStringList result;
205 for (const char* s : vec)
206 result.push_back(s);
207 return result;
208 };
209 const QStringList unmatchedPaths = vectorToList(row.unmatchedPaths);
210 const QStringList matchedPaths = vectorToList(row.matchedPaths);
211 #endif
212 QTest::newRow(row.dataTag) << formatDirs << formatContents << unmatchedPaths << matchedPaths;
213 }
214 }
215
testWildcardPathMatching()216 void TestKdevFormatSource::testWildcardPathMatching()
217 {
218 QFETCH(QStringList, formatDirs);
219 QFETCH(QByteArrayList, formatContents);
220 QFETCH(QStringList, unmatchedPaths);
221 QFETCH(QStringList, matchedPaths);
222
223 QTemporaryDir tmpDir;
224 QVERIFY2(tmpDir.isValid(), qPrintable("couldn't create temporary directory: " + tmpDir.errorString()));
225
226 using FilesystemHelpers::makeAbsoluteCreateAndWrite;
227
228 for (auto& dir : formatDirs) {
229 dir = QFileInfo{QDir{dir}, "format_sources"}.filePath();
230 }
231 QString errorPath = makeAbsoluteCreateAndWrite(tmpDir.path(), formatDirs, formatContents);
232 QVERIFY2(errorPath.isEmpty(), qPrintable("couldn't create or write to temporary file or directory " + errorPath));
233
234 errorPath = makeAbsoluteCreateAndWrite(tmpDir.path(), unmatchedPaths);
235 if (errorPath.isEmpty()) {
236 errorPath = makeAbsoluteCreateAndWrite(tmpDir.path(), matchedPaths);
237 }
238 QVERIFY2(errorPath.isEmpty(), qPrintable("couldn't create temporary file or directory " + errorPath));
239
240 bool expectedFormattingResult = false; // for unmatchedPaths
241 for (const auto& paths : { unmatchedPaths, matchedPaths }) {
242 for (const auto& path : paths) {
243 QVERIFY2(QFileInfo{path}.isFile(), qPrintable(path + ": file was not created or was deleted"));
244 const QString error = applyFormatting(path, expectedFormattingResult);
245 QVERIFY2(error.isEmpty(), qPrintable(error));
246 }
247 expectedFormattingResult = true; // for matchedPaths
248 }
249 }
250
initTest(const QStringList & formatFileData)251 bool TestKdevFormatSource::initTest(const QStringList& formatFileData)
252 {
253 QTest::addColumn<QString>("path");
254 QTest::addColumn<bool>("isFound");
255 QTest::addColumn<bool>("isRead");
256 QTest::addColumn<bool>("isApplied");
257 QTest::addColumn<QStringList>("lines");
258
259 m_temporaryDir.reset(new QTemporaryDir);
260 const QString workPath = m_temporaryDir->path();
261 qDebug() << "Using temporary dir:" << workPath;
262
263 if (!mkPath(workPath + "/src1"))
264 return false;
265
266 if (!mkPath(workPath + "/src2"))
267 return false;
268
269 if (!QDir::setCurrent(workPath)) {
270 qDebug() << "unable to set current directory to" << workPath;
271 return false;
272 }
273
274 m_sources.resize(3);
275
276 m_sources[0].path = workPath + "/src1/source_1.cpp";
277 m_sources[0].lines = QStringList({
278 QStringLiteral("void foo(int x) {"),
279 QStringLiteral(" printf(\"squared x = %d\\n\", x * x);"),
280 QStringLiteral("}")
281 });
282
283 m_sources[1].path = workPath + "/src2/source_2.cpp";
284 m_sources[1].lines = QStringList({
285 QStringLiteral("void bar(double x) {"),
286 QStringLiteral(" x = sqrt(x);"),
287 QStringLiteral(" printf(\"sqrt(x) = %e\\n\", x);"),
288 QStringLiteral("}")
289 });
290
291 m_sources[2].path = workPath + "/source_3.cpp";
292 m_sources[2].lines = QStringList({
293 QStringLiteral("void baz(double x, double y) {"),
294 QStringLiteral(" double z = pow(x, y);"),
295 QStringLiteral(" printf(\"x^y = %e\\n\", z);"),
296 QStringLiteral("}")
297 });
298
299 for (const Source& source : qAsConst(m_sources)) {
300 if (!writeLines(source.path, source.lines))
301 return false;
302 }
303
304 if (!formatFileData.isEmpty() && !writeLines(QStringLiteral("format_sources"), formatFileData))
305 return false;
306
307 return true;
308 }
309
runTest() const310 void TestKdevFormatSource::runTest() const
311 {
312 QFETCH(QString, path);
313 QFETCH(bool, isFound);
314 QFETCH(bool, isRead);
315 QFETCH(bool, isApplied);
316 QFETCH(QStringList, lines);
317
318 KDevFormatFile formatFile(path, path);
319
320 QCOMPARE(formatFile.find(), isFound);
321
322 if (isFound)
323 QCOMPARE(formatFile.read(), isRead);
324
325 if (isRead)
326 QCOMPARE(formatFile.apply(), isApplied);
327
328 QStringList processedLines;
329 QCOMPARE(readLines(path, processedLines), true);
330
331 QCOMPARE(processedLines, lines);
332 }
333
mkPath(const QString & path) const334 bool TestKdevFormatSource::mkPath(const QString& path) const
335 {
336 if (!QDir().exists(path) && !QDir().mkpath(path)) {
337 qDebug() << "unable to create directory" << path;
338 return false;
339 }
340
341 return true;
342 }
343
writeLines(const QString & path,const QStringList & lines) const344 bool TestKdevFormatSource::writeLines(const QString& path, const QStringList& lines) const
345 {
346 QFile outFile(path);
347 if (!outFile.open(QIODevice::WriteOnly)) {
348 qDebug() << "unable to open file" << path << "for writing";
349 return false;
350 }
351
352 QTextStream outStream(&outFile);
353 for (const QString& line : lines) {
354 outStream << line << "\n";
355 }
356
357 outStream.flush();
358 outFile.close();
359
360 return true;
361 }
362
readLines(const QString & path,QStringList & lines) const363 bool TestKdevFormatSource::readLines(const QString& path, QStringList& lines) const
364 {
365 QFile inFile(path);
366 if (!inFile.open(QIODevice::ReadOnly)) {
367 qDebug() << "unable to open file" << path << "for reading";
368 return false;
369 }
370
371 lines.clear();
372
373 QTextStream inStream(&inFile);
374 while (!inStream.atEnd()) {
375 lines += inStream.readLine();
376 }
377 inFile.close();
378
379 return true;
380 }
381