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