Skip to content
This repository was archived by the owner on Feb 4, 2020. It is now read-only.

Commit 61425b4

Browse files
authored
Merge pull request #179 from webmaster128/header-miss-tests
Handle missing and changed includes in direct mode
2 parents 0d623bb + 8e16bcd commit 61425b4

7 files changed

Lines changed: 184 additions & 44 deletions

File tree

clcache.py

Lines changed: 85 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,14 @@ def normalizeBaseDir(baseDir):
9191
return None
9292

9393

94+
class IncludeNotFoundException(Exception):
95+
pass
96+
97+
98+
class IncludeChangedException(Exception):
99+
pass
100+
101+
94102
class CacheLockException(Exception):
95103
pass
96104

@@ -118,7 +126,7 @@ def setManifest(self, manifestHash, manifest):
118126
ensureDirectoryExists(self.manifestSectionDir)
119127
with open(self.manifestPath(manifestHash), 'w') as outFile:
120128
# Converting namedtuple to JSON via OrderedDict preserves key names and keys order
121-
json.dump(manifest._asdict(), outFile, indent=2)
129+
json.dump(manifest._asdict(), outFile, sort_keys=True, indent=2)
122130

123131
def getManifest(self, manifestHash):
124132
fileName = self.manifestPath(manifestHash)
@@ -138,7 +146,7 @@ class ManifestRepository(object):
138146
# invalidation, such that a manifest that was stored using the old format is not
139147
# interpreted using the new format. Instead the old file will not be touched
140148
# again due to a new manifest hash and is cleaned away after some time.
141-
MANIFEST_FILE_FORMAT_VERSION = 3
149+
MANIFEST_FILE_FORMAT_VERSION = 4
142150

143151
def __init__(self, manifestsRootDir):
144152
self._manifestsRootDir = manifestsRootDir
@@ -184,8 +192,22 @@ def getManifestHash(compilerBinary, commandLine, sourceFile):
184192
return getFileHash(sourceFile, additionalData)
185193

186194
@staticmethod
187-
def getIncludesContentHashForFiles(listOfIncludesAbsolute):
188-
listOfIncludesHashes = [getFileHash(filepath) for filepath in listOfIncludesAbsolute]
195+
def getIncludesContentHashForFiles(includes):
196+
listOfIncludesHashes = []
197+
includeMissing = False
198+
199+
for path in sorted(includes.keys()):
200+
try:
201+
fileHash = getFileHash(path)
202+
if fileHash != includes[path]:
203+
raise IncludeChangedException()
204+
listOfIncludesHashes.append(fileHash)
205+
except FileNotFoundError:
206+
includeMissing = True
207+
208+
if includeMissing:
209+
raise IncludeNotFoundException()
210+
189211
return ManifestRepository.getIncludesContentHashForHashes(listOfIncludesHashes)
190212

191213
@staticmethod
@@ -1224,12 +1246,14 @@ def clearCache(cache):
12241246
print('Cache cleared')
12251247

12261248

1227-
# Returns pair - list of includes and new compiler output.
1249+
# Returns pair:
1250+
# 1. set of include filepaths
1251+
# 2. new compiler output
12281252
# Output changes if strip is True in that case all lines with include
12291253
# directives are stripped from it
1230-
def parseIncludesList(compilerOutput, sourceFile, strip):
1254+
def parseIncludesSet(compilerOutput, sourceFile, strip):
12311255
newOutput = []
1232-
includesSet = set([])
1256+
includesSet = set()
12331257

12341258
# Example lines
12351259
# Note: including file: C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\INCLUDE\limits.h
@@ -1256,9 +1280,9 @@ def parseIncludesList(compilerOutput, sourceFile, strip):
12561280
elif strip:
12571281
newOutput.append(line)
12581282
if strip:
1259-
return sorted(includesSet), ''.join(newOutput)
1283+
return includesSet, ''.join(newOutput)
12601284
else:
1261-
return sorted(includesSet), compilerOutput
1285+
return includesSet, compilerOutput
12621286

12631287

12641288
def addObjectToCache(stats, cache, cachekey, artifacts):
@@ -1294,17 +1318,34 @@ def postprocessObjectEvicted(cache, objectFile, cachekey, compilerResult):
12941318
return compilerResult
12951319

12961320

1297-
def postprocessHeaderChangedMiss(
1298-
cache, objectFile, manifestSection, manifest, manifestHash, includesContentHash, compilerResult):
1321+
def createManifest(manifestHash, includePaths):
1322+
baseDir = normalizeBaseDir(os.environ.get('CLCACHE_BASEDIR'))
1323+
1324+
includes = {path:getFileHash(path) for path in includePaths}
1325+
includesContentHash = ManifestRepository.getIncludesContentHashForFiles(includes)
12991326
cachekey = Cache.getDirectCacheKey(manifestHash, includesContentHash)
1327+
1328+
# Create new manifest
1329+
if baseDir:
1330+
relocatableIncludePaths = {
1331+
collapseBasedirToPlaceholder(path, baseDir):contentHash
1332+
for path, contentHash in includes.items()
1333+
}
1334+
manifest = Manifest(relocatableIncludePaths, {})
1335+
else:
1336+
manifest = Manifest(includes, {})
1337+
manifest.includesContentToObjectMap[includesContentHash] = cachekey
1338+
return manifest, cachekey
1339+
1340+
1341+
def postprocessHeaderChangedMiss(
1342+
cache, objectFile, manifestSection, manifestHash, sourceFile, compilerResult, stripIncludes):
13001343
returnCode, compilerOutput, compilerStderr = compilerResult
1344+
includePaths, compilerOutput = parseIncludesSet(compilerOutput, sourceFile, stripIncludes)
13011345

13021346
removedItems = []
13031347
if returnCode == 0 and os.path.exists(objectFile):
1304-
while len(manifest.includesContentToObjectMap) >= MAX_MANIFEST_HASHES:
1305-
_, objectHash = manifest.includesContentToObjectMap.popitem()
1306-
removedItems.append(objectHash)
1307-
manifest.includesContentToObjectMap[includesContentHash] = cachekey
1348+
manifest, cachekey = createManifest(manifestHash, includePaths)
13081349

13091350
with cache.lock, cache.statistics as stats:
13101351
stats.registerHeaderChangedMiss()
@@ -1313,27 +1354,19 @@ def postprocessHeaderChangedMiss(
13131354
cache.compilerArtifactsRepository.removeObjects(stats, removedItems)
13141355
manifestSection.setManifest(manifestHash, manifest)
13151356

1316-
return compilerResult
1357+
return returnCode, compilerOutput, compilerStderr
13171358

13181359

13191360
def postprocessNoManifestMiss(
1320-
cache, objectFile, manifestSection, manifestHash, baseDir, sourceFile, compilerResult, stripIncludes):
1361+
cache, objectFile, manifestSection, manifestHash, sourceFile, compilerResult, stripIncludes):
13211362
returnCode, compilerOutput, compilerStderr = compilerResult
1322-
listOfIncludes, compilerOutput = parseIncludesList(compilerOutput, sourceFile, stripIncludes)
1363+
includePaths, compilerOutput = parseIncludesSet(compilerOutput, sourceFile, stripIncludes)
13231364

13241365
manifest = None
13251366
cachekey = None
13261367

13271368
if returnCode == 0 and os.path.exists(objectFile):
1328-
# Store compile output and manifest
1329-
if baseDir:
1330-
relocatableIncludePaths = [collapseBasedirToPlaceholder(path, baseDir) for path in listOfIncludes]
1331-
manifest = Manifest(relocatableIncludePaths, {})
1332-
else:
1333-
manifest = Manifest(listOfIncludes, {})
1334-
includesContentHash = ManifestRepository.getIncludesContentHashForFiles(listOfIncludes)
1335-
cachekey = Cache.getDirectCacheKey(manifestHash, includesContentHash)
1336-
manifest.includesContentToObjectMap[includesContentHash] = cachekey
1369+
manifest, cachekey = createManifest(manifestHash, includePaths)
13371370

13381371
with cache.lock, cache.statistics as stats:
13391372
stats.registerSourceChangedMiss()
@@ -1469,32 +1502,44 @@ def processDirect(cache, objectFile, compiler, cmdLine, sourceFile):
14691502
manifestHash = ManifestRepository.getManifestHash(compiler, cmdLine, sourceFile)
14701503
manifestSection = cache.manifestRepository.section(manifestHash)
14711504
with cache.lock:
1505+
createNewManifest = False
14721506
manifest = manifestSection.getManifest(manifestHash)
14731507
if manifest is not None:
14741508
# NOTE: command line options already included in hash for manifest name
1475-
includesContentHash = ManifestRepository.getIncludesContentHashForFiles(
1476-
[expandBasedirPlaceholder(include, baseDir) for include in manifest.includeFiles])
1477-
cachekey = manifest.includesContentToObjectMap.get(includesContentHash)
1478-
if cachekey is not None:
1509+
try:
1510+
includesContentHash = ManifestRepository.getIncludesContentHashForFiles({
1511+
expandBasedirPlaceholder(path, baseDir):contentHash
1512+
for path, contentHash in manifest.includeFiles.items()
1513+
})
1514+
1515+
cachekey = manifest.includesContentToObjectMap.get(includesContentHash)
1516+
assert cachekey is not None
14791517
if cache.compilerArtifactsRepository.section(cachekey).hasEntry(cachekey):
14801518
return processCacheHit(cache, objectFile, cachekey)
14811519
else:
14821520
postProcessing = lambda compilerResult: postprocessObjectEvicted(
14831521
cache, objectFile, cachekey, compilerResult)
1484-
else:
1522+
except IncludeChangedException:
1523+
createNewManifest = True
14851524
postProcessing = lambda compilerResult: postprocessHeaderChangedMiss(
1486-
cache, objectFile, manifestSection, manifest, manifestHash, includesContentHash, compilerResult)
1525+
cache, objectFile, manifestSection, manifestHash, sourceFile, compilerResult, stripIncludes)
1526+
except IncludeNotFoundException:
1527+
# register nothing. This is probably just a compile error
1528+
postProcessing = None
14871529
else:
1488-
origCmdLine = cmdLine
1489-
stripIncludes = False
1490-
if '/showIncludes' not in cmdLine:
1491-
cmdLine = ['/showIncludes'] + origCmdLine
1492-
stripIncludes = True
1530+
createNewManifest = True
14931531
postProcessing = lambda compilerResult: postprocessNoManifestMiss(
1494-
cache, objectFile, manifestSection, manifestHash, baseDir, sourceFile, compilerResult, stripIncludes)
1532+
cache, objectFile, manifestSection, manifestHash, sourceFile, compilerResult, stripIncludes)
1533+
1534+
if createNewManifest:
1535+
stripIncludes = False
1536+
if '/showIncludes' not in cmdLine:
1537+
cmdLine.insert(0, '/showIncludes')
1538+
stripIncludes = True
14951539

14961540
compilerResult = invokeRealCompiler(compiler, cmdLine, captureOutput=True)
1497-
compilerResult = postProcessing(compilerResult)
1541+
if postProcessing:
1542+
compilerResult = postProcessing(compilerResult)
14981543
printTraceStatement("Finished. Exit code {0:d}".format(compilerResult[0]))
14991544
return compilerResult
15001545

integrationtests.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,76 @@ def testNoDirect(self):
312312
self.assertEqual(output, "2")
313313

314314

315+
class TestHeaderMiss(unittest.TestCase):
316+
# When a required header disappears, we must fall back to real compiler
317+
# complaining about the miss
318+
def testRequiredHeaderDisappears(self):
319+
with cd(os.path.join(ASSETS_DIR, "header-miss")):
320+
compileCmd = CLCACHE_CMD + ["/nologo", "/EHsc", "/c", "main.cpp"]
321+
322+
with open("info.h", "w") as header:
323+
header.write("#define INFO 1337\n")
324+
subprocess.check_call(compileCmd)
325+
326+
os.remove("info.h")
327+
328+
# real compiler fails
329+
process = subprocess.Popen(compileCmd, stdout=subprocess.PIPE)
330+
stdout, _ = process.communicate()
331+
self.assertEqual(process.returncode, 2)
332+
self.assertTrue("C1083" in stdout.decode(clcache.CL_DEFAULT_CODEC))
333+
334+
# When a header included by another header becomes obsolete and disappers,
335+
# we must fall back to real compiler.
336+
def testObsoleteHeaderDisappears(self):
337+
# A includes B
338+
with cd(os.path.join(ASSETS_DIR, "header-miss-obsolete")):
339+
compileCmd = CLCACHE_CMD + ["/I.", "/nologo", "/EHsc", "/c", "main.cpp"]
340+
cache = clcache.Cache()
341+
342+
with open("A.h", "w") as header:
343+
header.write('#define INFO 1337\n')
344+
header.write('#include "B.h"\n')
345+
with open("B.h", "w") as header:
346+
header.write('#define SOMETHING 1\n')
347+
348+
subprocess.check_call(compileCmd)
349+
350+
with cache.statistics as stats:
351+
headerChangedMisses1 = stats.numHeaderChangedMisses()
352+
hits1 = stats.numCacheHits()
353+
misses1 = stats.numCacheMisses()
354+
355+
# Make include B.h obsolete
356+
with open("A.h", "w") as header:
357+
header.write('#define INFO 1337\n')
358+
header.write('\n')
359+
os.remove("B.h")
360+
361+
subprocess.check_call(compileCmd)
362+
363+
with cache.statistics as stats:
364+
headerChangedMisses2 = stats.numHeaderChangedMisses()
365+
hits2 = stats.numCacheHits()
366+
misses2 = stats.numCacheMisses()
367+
368+
self.assertEqual(headerChangedMisses2, headerChangedMisses1+1)
369+
self.assertEqual(misses2, misses1+1)
370+
self.assertEqual(hits2, hits1)
371+
372+
# Ensure the new manifest was stored
373+
subprocess.check_call(compileCmd)
374+
375+
with cache.statistics as stats:
376+
headerChangedMisses3 = stats.numHeaderChangedMisses()
377+
hits3 = stats.numCacheHits()
378+
misses3 = stats.numCacheMisses()
379+
380+
self.assertEqual(headerChangedMisses3, headerChangedMisses2)
381+
self.assertEqual(misses3, misses2)
382+
self.assertEqual(hits3, hits2+1)
383+
384+
315385
class TestRunParallel(unittest.TestCase):
316386
def _zeroStats(self):
317387
subprocess.check_call(CLCACHE_CMD + ["-z"])
@@ -604,6 +674,8 @@ def testHitsViaMpConcurrent(self):
604674
self.assertEqual(stats.numCacheHits(), 2)
605675
self.assertEqual(stats.numCacheMisses(), 2)
606676
self.assertEqual(stats.numCacheEntries(), 2)
677+
678+
607679
class TestBasedir(unittest.TestCase):
608680
def testBasedir(self):
609681
with cd(os.path.join(ASSETS_DIR, "basedir")), tempfile.TemporaryDirectory() as tempDir:
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
*.obj
2+
3+
# headers auto-generated by tests
4+
A.h
5+
B.h
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#include <iostream>
2+
#include "A.h"
3+
4+
int main()
5+
{
6+
std::cout << INFO << std::endl;
7+
return 0;
8+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# This file is auto-generated by tests
2+
info.h
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#include <iostream>
2+
#include "info.h"
3+
4+
int main()
5+
{
6+
std::cout << INFO << std::endl;
7+
return 0;
8+
}

unittests.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -812,7 +812,7 @@ def _readSampleFileNoIncludes(self):
812812

813813
def testParseIncludesNoStrip(self):
814814
sample = self._readSampleFileDefault()
815-
includesSet, newCompilerOutput = clcache.parseIncludesList(
815+
includesSet, newCompilerOutput = clcache.parseIncludesSet(
816816
sample['CompilerOutput'],
817817
r'C:\Projects\test\smartsqlite\src\version.cpp',
818818
strip=False)
@@ -826,7 +826,7 @@ def testParseIncludesNoStrip(self):
826826

827827
def testParseIncludesStrip(self):
828828
sample = self._readSampleFileDefault()
829-
includesSet, newCompilerOutput = clcache.parseIncludesList(
829+
includesSet, newCompilerOutput = clcache.parseIncludesSet(
830830
sample['CompilerOutput'],
831831
r'C:\Projects\test\smartsqlite\src\version.cpp',
832832
strip=True)
@@ -841,7 +841,7 @@ def testParseIncludesStrip(self):
841841
def testParseIncludesNoIncludes(self):
842842
sample = self._readSampleFileNoIncludes()
843843
for stripIncludes in [True, False]:
844-
includesSet, newCompilerOutput = clcache.parseIncludesList(
844+
includesSet, newCompilerOutput = clcache.parseIncludesSet(
845845
sample['CompilerOutput'],
846846
r"C:\Projects\test\myproject\main.cpp",
847847
strip=stripIncludes)
@@ -851,7 +851,7 @@ def testParseIncludesNoIncludes(self):
851851

852852
def testParseIncludesGerman(self):
853853
sample = self._readSampleFileDefault(lang="de")
854-
includesSet, _ = clcache.parseIncludesList(
854+
includesSet, _ = clcache.parseIncludesSet(
855855
sample['CompilerOutput'],
856856
r"C:\Projects\test\smartsqlite\src\version.cpp",
857857
strip=False)

0 commit comments

Comments
 (0)