build(broccoli): add tree-differ for diffing broccoli trees
This commit is contained in:
		
							parent
							
								
									32c5ab956c
								
							
						
					
					
						commit
						2f83efaac8
					
				
							
								
								
									
										190
									
								
								tools/broccoli/tree-differ.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										190
									
								
								tools/broccoli/tree-differ.spec.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,190 @@ | ||||
| /// <reference path="../typings/node/node.d.ts" />
 | ||||
| /// <reference path="../typings/jasmine/jasmine.d.ts" />
 | ||||
| 
 | ||||
| let mockfs = require('mock-fs'); | ||||
| import fs = require('fs'); | ||||
| import TreeDiffer = require('./tree-differ'); | ||||
| 
 | ||||
| 
 | ||||
| describe('TreeDiffer', () => { | ||||
| 
 | ||||
|   afterEach(() => mockfs.restore()); | ||||
| 
 | ||||
| 
 | ||||
|   describe('diff of changed files', () => { | ||||
| 
 | ||||
|     it('should list all files but no directories during the first diff', () => { | ||||
|       let testDir = { | ||||
|         'dir1': { | ||||
|           'file-1.txt': mockfs.file({content: 'file-1.txt content', mtime: new Date(1000)}), | ||||
|           'file-2.txt': mockfs.file({content: 'file-2.txt content', mtime: new Date(1000)}), | ||||
|           'subdir-1': { | ||||
|             'file-1.1.txt': mockfs.file({content: 'file-1.1.txt content', mtime: new Date(1000)}) | ||||
|           }, | ||||
|           'empty-dir': {} | ||||
|         } | ||||
|       }; | ||||
|       mockfs(testDir); | ||||
| 
 | ||||
|       let differ = new TreeDiffer('dir1'); | ||||
| 
 | ||||
|       let diffResult = differ.diffTree(); | ||||
| 
 | ||||
|       expect(diffResult.changedPaths) | ||||
|           .toEqual(['file-1.txt', 'file-2.txt', 'subdir-1/file-1.1.txt']); | ||||
| 
 | ||||
|       expect(diffResult.removedPaths).toEqual([]); | ||||
|     }); | ||||
| 
 | ||||
| 
 | ||||
|     it('should return empty diff if nothing has changed', () => { | ||||
|       let testDir = { | ||||
|         'dir1': { | ||||
|           'file-1.txt': mockfs.file({content: 'file-1.txt content', mtime: new Date(1000)}), | ||||
|           'file-2.txt': mockfs.file({content: 'file-2.txt content', mtime: new Date(1000)}), | ||||
|           'subdir-1': { | ||||
|             'file-1.1.txt': mockfs.file({content: 'file-1.1.txt content', mtime: new Date(1000)}) | ||||
|           }, | ||||
|         } | ||||
|       }; | ||||
|       mockfs(testDir); | ||||
| 
 | ||||
|       let differ = new TreeDiffer('dir1'); | ||||
| 
 | ||||
|       let diffResult = differ.diffTree(); | ||||
| 
 | ||||
|       expect(diffResult.changedPaths).not.toEqual([]); | ||||
|       expect(diffResult.removedPaths).toEqual([]); | ||||
| 
 | ||||
|       diffResult = differ.diffTree(); | ||||
| 
 | ||||
|       expect(diffResult.changedPaths).toEqual([]); | ||||
|       expect(diffResult.removedPaths).toEqual([]); | ||||
|     }); | ||||
| 
 | ||||
| 
 | ||||
|     it('should list only changed files during the subsequent diffs', () => { | ||||
|       let testDir = { | ||||
|         'dir1': { | ||||
|           'file-1.txt': mockfs.file({content: 'file-1.txt content', mtime: new Date(1000)}), | ||||
|           'file-2.txt': mockfs.file({content: 'file-2.txt content', mtime: new Date(1000)}), | ||||
|           'subdir-1': { | ||||
|             'file-1.1.txt': | ||||
|                 mockfs.file({content: 'file-1.1.txt content', mtime: new Date(1000)}) | ||||
|           } | ||||
|         } | ||||
|       }; | ||||
|       mockfs(testDir); | ||||
| 
 | ||||
|       let differ = new TreeDiffer('dir1'); | ||||
| 
 | ||||
|       let diffResult = differ.diffTree(); | ||||
| 
 | ||||
|       expect(diffResult.changedPaths) | ||||
|           .toEqual(['file-1.txt', 'file-2.txt', 'subdir-1/file-1.1.txt']); | ||||
| 
 | ||||
|       // change two files
 | ||||
|       testDir['dir1']['file-1.txt'] = mockfs.file({content: 'new content', mtime: new Date(1000)}); | ||||
|       testDir['dir1']['subdir-1']['file-1.1.txt'] = | ||||
|           mockfs.file({content: 'file-1.1.txt content', mtime: new Date(9999)}); | ||||
|       mockfs(testDir); | ||||
| 
 | ||||
|       diffResult = differ.diffTree(); | ||||
| 
 | ||||
|       expect(diffResult.changedPaths).toEqual(['file-1.txt', 'subdir-1/file-1.1.txt']); | ||||
| 
 | ||||
|       expect(diffResult.removedPaths).toEqual([]); | ||||
| 
 | ||||
|       // change one file
 | ||||
|       testDir['dir1']['file-1.txt'] = mockfs.file({content: 'super new', mtime: new Date(1000)}); | ||||
|       mockfs(testDir); | ||||
| 
 | ||||
|       diffResult = differ.diffTree(); | ||||
|       expect(diffResult.changedPaths).toEqual(['file-1.txt']); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('diff of new files', () => { | ||||
| 
 | ||||
|     it('should detect file additions and report them as changed files', () => { | ||||
|       let testDir = { | ||||
|         'dir1': | ||||
|             {'file-1.txt': mockfs.file({content: 'file-1.txt content', mtime: new Date(1000)})} | ||||
|       }; | ||||
|       mockfs(testDir); | ||||
| 
 | ||||
|       let differ = new TreeDiffer('dir1'); | ||||
|       differ.diffTree(); | ||||
| 
 | ||||
|       testDir['dir1']['file-2.txt'] = 'new file'; | ||||
|       mockfs(testDir); | ||||
| 
 | ||||
|       let diffResult = differ.diffTree(); | ||||
|       expect(diffResult.changedPaths).toEqual(['file-2.txt']); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
| 
 | ||||
|   it('should detect file additions mixed with file changes', () => { | ||||
|     let testDir = { | ||||
|       'dir1': {'file-1.txt': mockfs.file({content: 'file-1.txt content', mtime: new Date(1000)})} | ||||
|     }; | ||||
|     mockfs(testDir); | ||||
| 
 | ||||
|     let differ = new TreeDiffer('dir1'); | ||||
|     differ.diffTree(); | ||||
| 
 | ||||
|     testDir['dir1']['file-1.txt'] = 'new content'; | ||||
|     testDir['dir1']['file-2.txt'] = 'new file'; | ||||
|     mockfs(testDir); | ||||
| 
 | ||||
|     let diffResult = differ.diffTree(); | ||||
|     expect(diffResult.changedPaths).toEqual(['file-1.txt', 'file-2.txt']); | ||||
|   }); | ||||
| 
 | ||||
| 
 | ||||
|   describe('diff of removed files', () => { | ||||
| 
 | ||||
|     it('should detect file removals and report them as removed files', () => { | ||||
|       let testDir = { | ||||
|         'dir1': | ||||
|             {'file-1.txt': mockfs.file({content: 'file-1.txt content', mtime: new Date(1000)})} | ||||
|       }; | ||||
|       mockfs(testDir); | ||||
| 
 | ||||
|       let differ = new TreeDiffer('dir1'); | ||||
|       differ.diffTree(); | ||||
| 
 | ||||
|       delete testDir['dir1']['file-1.txt']; | ||||
|       mockfs(testDir); | ||||
| 
 | ||||
|       let diffResult = differ.diffTree(); | ||||
|       expect(diffResult.changedPaths).toEqual([]); | ||||
|       expect(diffResult.removedPaths).toEqual(['file-1.txt']); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
| 
 | ||||
|   it('should detect file removals mixed with file changes and additions', () => { | ||||
|     let testDir = { | ||||
|       'dir1': { | ||||
|         'file-1.txt': mockfs.file({content: 'file-1.txt content', mtime: new Date(1000)}), | ||||
|         'file-2.txt': mockfs.file({content: 'file-1.txt content', mtime: new Date(1000)}) | ||||
|       } | ||||
|     }; | ||||
| 
 | ||||
|     mockfs(testDir); | ||||
| 
 | ||||
|     let differ = new TreeDiffer('dir1'); | ||||
|     differ.diffTree(); | ||||
| 
 | ||||
|     testDir['dir1']['file-1.txt'] = 'changed content'; | ||||
|     delete testDir['dir1']['file-2.txt']; | ||||
|     testDir['dir1']['file-3.txt'] = 'new content'; | ||||
|     mockfs(testDir); | ||||
| 
 | ||||
|     let diffResult = differ.diffTree(); | ||||
|     expect(diffResult.changedPaths).toEqual(['file-1.txt', 'file-3.txt']); | ||||
|     expect(diffResult.removedPaths).toEqual(['file-2.txt']); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										110
									
								
								tools/broccoli/tree-differ.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								tools/broccoli/tree-differ.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,110 @@ | ||||
| /// <reference path="../typings/node/node.d.ts" />
 | ||||
| 
 | ||||
| import fs = require('fs'); | ||||
| import path = require('path'); | ||||
| 
 | ||||
| 
 | ||||
| export = TreeDiffer; | ||||
| 
 | ||||
| class TreeDiffer { | ||||
|   private fingerprints: {[key: string]: string} = Object.create(null); | ||||
|   private nextFingerprints: {[key: string]: string} = Object.create(null); | ||||
|   private rootDirName: string; | ||||
| 
 | ||||
|   constructor(private rootPath: string) { this.rootDirName = path.basename(rootPath); } | ||||
| 
 | ||||
| 
 | ||||
|   public diffTree(): DiffResult { | ||||
|     let result = new DiffResult(this.rootDirName); | ||||
|     this.dirtyCheckPath(this.rootPath, result); | ||||
|     this.detectDeletionsAndUpdateFingerprints(result); | ||||
|     result.endTime = Date.now(); | ||||
|     return result; | ||||
|   } | ||||
| 
 | ||||
| 
 | ||||
|   private dirtyCheckPath(rootDir: string, result: DiffResult) { | ||||
|     fs.readdirSync(rootDir).forEach((segment) => { | ||||
|       let absolutePath = path.join(rootDir, segment); | ||||
|       let pathStat = fs.statSync(absolutePath); | ||||
| 
 | ||||
|       if (pathStat.isDirectory()) { | ||||
|         result.directoriesChecked++; | ||||
|         this.dirtyCheckPath(absolutePath, result); | ||||
|       } else { | ||||
|         result.filesChecked++; | ||||
|         if (this.isFileDirty(absolutePath, pathStat)) { | ||||
|           result.changedPaths.push(path.relative(this.rootPath, absolutePath)); | ||||
|         } | ||||
|       } | ||||
|     }); | ||||
| 
 | ||||
|     return result; | ||||
|   } | ||||
| 
 | ||||
| 
 | ||||
|   private isFileDirty(path: string, stat: fs.Stats): boolean { | ||||
|     let oldFingerprint = this.fingerprints[path]; | ||||
|     let newFingerprint = `${stat.mtime.getTime()} # ${stat.size}`; | ||||
| 
 | ||||
|     this.nextFingerprints[path] = newFingerprint; | ||||
| 
 | ||||
|     if (oldFingerprint) { | ||||
|       this.fingerprints[path] = null; | ||||
| 
 | ||||
|       if (oldFingerprint === newFingerprint) { | ||||
|         // nothing changed
 | ||||
|         return false; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     return true; | ||||
|   } | ||||
| 
 | ||||
| 
 | ||||
|   private detectDeletionsAndUpdateFingerprints(result: DiffResult) { | ||||
|     for (let absolutePath in this.fingerprints) { | ||||
|       if (this.fingerprints[absolutePath] !== null) { | ||||
|         let relativePath = path.relative(this.rootPath, absolutePath); | ||||
|         result.removedPaths.push(relativePath); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     this.fingerprints = this.nextFingerprints; | ||||
|     this.nextFingerprints = Object.create(null); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| class DiffResult { | ||||
|   public filesChecked: number = 0; | ||||
|   public directoriesChecked: number = 0; | ||||
|   public changedPaths: string[] = []; | ||||
|   public removedPaths: string[] = []; | ||||
|   public startTime: number = Date.now(); | ||||
|   public endTime: number = null; | ||||
| 
 | ||||
|   constructor(public name: string) {} | ||||
| 
 | ||||
|   toString() { | ||||
|     return `${pad(this.name, 40)}, ` + | ||||
|            `duration: ${pad(this.endTime - this.startTime, 5)}ms, ` + | ||||
|            `${pad(this.changedPaths.length + this.removedPaths.length, 5)} changes detected ` + | ||||
|            `(files: ${pad(this.filesChecked, 5)}, directories: ${pad(this.directoriesChecked, 4)})`; | ||||
|   } | ||||
| 
 | ||||
|   log(verbose) { | ||||
|     let prefixedPaths = | ||||
|         this.changedPaths.map((p) => `* ${p}`).concat(this.removedPaths.map((p) => `- ${p}`)); | ||||
|     console.log(`Tree diff: ${this}` + | ||||
|                 ((verbose && prefixedPaths.length) ? ` [\n  ${prefixedPaths.join('\n  ')}\n]` :  '')); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| function pad(value, length) { | ||||
|   value = '' + value; | ||||
|   let whitespaceLength = (value.length < length) ? length - value.length : 0; | ||||
|   whitespaceLength = whitespaceLength + 1; | ||||
|   return new Array(whitespaceLength).join(' ') + value; | ||||
| } | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user