Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Update license tests in preparation for new year
- Loading branch information
1 parent
1225d45
commit 116ff74
Showing
4 changed files
with
205 additions
and
101 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,115 +1,52 @@ | ||
// Copyright 2020 Signal Messenger, LLC | ||
// SPDX-License-Identifier: AGPL-3.0-only | ||
|
||
import { assert } from 'chai'; | ||
import * as path from 'path'; | ||
import * as fs from 'fs'; | ||
import { promisify } from 'util'; | ||
import * as readline from 'readline'; | ||
import * as childProcess from 'child_process'; | ||
import pMap from 'p-map'; | ||
|
||
const exec = promisify(childProcess.exec); | ||
|
||
const EXTENSIONS_TO_CHECK = new Set([ | ||
'.eslintignore', | ||
'.gitattributes', | ||
'.gitignore', | ||
'.nvmrc', | ||
'.prettierignore', | ||
'.sh', | ||
'.snyk', | ||
'.yarnclean', | ||
'.yml', | ||
'.js', | ||
'.scss', | ||
'.ts', | ||
'.tsx', | ||
'.html', | ||
'.md', | ||
'.plist', | ||
]); | ||
const FILES_TO_IGNORE = new Set([ | ||
'ISSUE_TEMPLATE.md', | ||
'Mp3LameEncoder.min.js', | ||
'PULL_REQUEST_TEMPLATE.md', | ||
'WebAudioRecorderMp3.js', | ||
]); | ||
|
||
const rootPath = path.join(__dirname, '..', '..'); | ||
|
||
async function getGitFiles(): Promise<Array<string>> { | ||
return (await exec('git ls-files', { cwd: rootPath, env: {} })).stdout | ||
.split(/\n/g) | ||
.map(line => line.trim()) | ||
.filter(Boolean) | ||
.map(file => path.join(rootPath, file)); | ||
} | ||
// This file is meant to be run frequently, so it doesn't check the license year. See the | ||
// imported `license_comments` file for a job that does this, to be run on CI. | ||
|
||
// This is not technically the real extension. | ||
function getExtension(file: string): string { | ||
if (file.startsWith('.')) { | ||
return getExtension(`x.${file}`); | ||
} | ||
return path.extname(file); | ||
} | ||
|
||
function readFirstTwoLines(file: string): Promise<Array<string>> { | ||
return new Promise(resolve => { | ||
const lines: Array<string> = []; | ||
import { assert } from 'chai'; | ||
|
||
const lineReader = readline.createInterface({ | ||
input: fs.createReadStream(file), | ||
}); | ||
lineReader.on('line', line => { | ||
lines.push(line); | ||
if (lines.length >= 2) { | ||
lineReader.close(); | ||
} | ||
}); | ||
lineReader.on('close', () => { | ||
resolve(lines); | ||
}); | ||
}); | ||
} | ||
import { | ||
forEachRelevantFile, | ||
readFirstLines, | ||
} from '../util/lint/license_comments'; | ||
|
||
describe('license comments', () => { | ||
it('includes a license comment at the top of every relevant file', async function test() { | ||
// This usually executes quickly but can be slow in some cases, such as Windows CI. | ||
this.timeout(10000); | ||
|
||
const currentYear = new Date().getFullYear(); | ||
|
||
await pMap( | ||
await getGitFiles(), | ||
async (file: string) => { | ||
if ( | ||
FILES_TO_IGNORE.has(path.basename(file)) || | ||
path.relative(rootPath, file).startsWith('components') | ||
) { | ||
return; | ||
} | ||
|
||
const extension = getExtension(file); | ||
if (!EXTENSIONS_TO_CHECK.has(extension)) { | ||
return; | ||
} | ||
|
||
const [firstLine, secondLine] = await readFirstTwoLines(file); | ||
|
||
assert.match( | ||
firstLine, | ||
RegExp(`Copyright (?:\\d{4}-)?${currentYear} Signal Messenger, LLC`), | ||
`First line of ${file} is missing correct license header comment` | ||
); | ||
assert.include( | ||
secondLine, | ||
'SPDX-License-Identifier: AGPL-3.0-only', | ||
`Second line of ${file} is missing correct license header comment` | ||
await forEachRelevantFile(async file => { | ||
const [firstLine, secondLine] = await readFirstLines(file, 2); | ||
|
||
const { groups = {} } = | ||
firstLine.match( | ||
/Copyright (?<startYearWithDash>\d{4}-)?(?<endYearString>\d{4}) Signal Messenger, LLC/ | ||
) || []; | ||
const { startYearWithDash, endYearString } = groups; | ||
const endYear = Number(endYearString); | ||
|
||
// We added these comments in 2020. | ||
assert.isAtLeast( | ||
endYear, | ||
2020, | ||
`First line of ${file} is missing correct license header comment` | ||
); | ||
|
||
if (startYearWithDash) { | ||
const startYear = Number(startYearWithDash.slice(0, -1)); | ||
assert.isBelow( | ||
startYear, | ||
endYear, | ||
`Starting license year of ${file} is not below the ending year` | ||
); | ||
}, | ||
// Without this, we may run into "too many open files" errors. | ||
{ concurrency: 100 } | ||
); | ||
} | ||
|
||
assert.include( | ||
secondLine, | ||
'SPDX-License-Identifier: AGPL-3.0-only', | ||
`Second line of ${file} is missing correct license header comment` | ||
); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,165 @@ | ||
// Copyright 2020 Signal Messenger, LLC | ||
// SPDX-License-Identifier: AGPL-3.0-only | ||
|
||
// This file doesn't check the format of license files, just the end year. See | ||
// `license_comments_test.ts` for those checks, which are meant to be run more often. | ||
|
||
import assert from 'assert'; | ||
import * as readline from 'readline'; | ||
import * as path from 'path'; | ||
import * as fs from 'fs'; | ||
import { promisify } from 'util'; | ||
import * as childProcess from 'child_process'; | ||
import pMap from 'p-map'; | ||
|
||
const exec = promisify(childProcess.exec); | ||
|
||
const rootPath = path.join(__dirname, '..', '..'); | ||
|
||
const EXTENSIONS_TO_CHECK = new Set([ | ||
'.eslintignore', | ||
'.gitattributes', | ||
'.gitignore', | ||
'.nvmrc', | ||
'.prettierignore', | ||
'.sh', | ||
'.snyk', | ||
'.yarnclean', | ||
'.yml', | ||
'.js', | ||
'.scss', | ||
'.ts', | ||
'.tsx', | ||
'.html', | ||
'.md', | ||
'.plist', | ||
]); | ||
const FILES_TO_IGNORE = new Set([ | ||
'ISSUE_TEMPLATE.md', | ||
'Mp3LameEncoder.min.js', | ||
'PULL_REQUEST_TEMPLATE.md', | ||
'WebAudioRecorderMp3.js', | ||
]); | ||
|
||
// This is not technically the real extension. | ||
function getExtension(file: string): string { | ||
if (file.startsWith('.')) { | ||
return getExtension(`x.${file}`); | ||
} | ||
return path.extname(file); | ||
} | ||
|
||
export async function forEachRelevantFile( | ||
fn: (_: string) => Promise<unknown> | ||
): Promise<void> { | ||
const gitFiles = ( | ||
await exec('git ls-files', { cwd: rootPath, env: {} }) | ||
).stdout | ||
.split(/\n/g) | ||
.map(line => line.trim()) | ||
.filter(Boolean) | ||
.map(file => path.join(rootPath, file)); | ||
|
||
await pMap( | ||
gitFiles, | ||
async (file: string) => { | ||
if ( | ||
FILES_TO_IGNORE.has(path.basename(file)) || | ||
path.relative(rootPath, file).startsWith('components') | ||
) { | ||
return; | ||
} | ||
|
||
const extension = getExtension(file); | ||
if (!EXTENSIONS_TO_CHECK.has(extension)) { | ||
return; | ||
} | ||
|
||
await fn(file); | ||
}, | ||
// Without this, we may run into "too many open files" errors. | ||
{ concurrency: 100 } | ||
); | ||
} | ||
|
||
export function readFirstLines( | ||
file: string, | ||
count: number | ||
): Promise<Array<string>> { | ||
return new Promise(resolve => { | ||
const lines: Array<string> = []; | ||
|
||
const lineReader = readline.createInterface({ | ||
input: fs.createReadStream(file), | ||
}); | ||
lineReader.on('line', line => { | ||
lines.push(line); | ||
if (lines.length >= count) { | ||
lineReader.close(); | ||
} | ||
}); | ||
lineReader.on('close', () => { | ||
resolve(lines); | ||
}); | ||
}); | ||
} | ||
|
||
async function getLatestCommitYearForFile(file: string): Promise<number> { | ||
const dateString = ( | ||
await new Promise<string>((resolve, reject) => { | ||
let result = ''; | ||
// We use the more verbose `spawn` to avoid command injection, in case the filename | ||
// has strange characters. | ||
const gitLog = childProcess.spawn( | ||
'git', | ||
['log', '-1', '--format=%as', file], | ||
{ | ||
cwd: rootPath, | ||
env: { PATH: process.env.PATH }, | ||
} | ||
); | ||
gitLog.stdout?.on('data', data => { | ||
result += data.toString('utf8'); | ||
}); | ||
gitLog.on('close', code => { | ||
if (code === 0) { | ||
resolve(result); | ||
} else { | ||
reject(new Error(`git log failed with exit code ${code}`)); | ||
} | ||
}); | ||
}) | ||
).trim(); | ||
|
||
const result = new Date(dateString).getFullYear(); | ||
assert(!Number.isNaN(result), `Could not read commit year for ${file}`); | ||
return result; | ||
} | ||
|
||
async function main() { | ||
const currentYear = new Date().getFullYear() + 1; | ||
|
||
await forEachRelevantFile(async file => { | ||
const [firstLine] = await readFirstLines(file, 1); | ||
const { groups = {} } = | ||
firstLine.match(/(?:\d{4}-)?(?<endYearString>\d{4})/) || []; | ||
const { endYearString } = groups; | ||
const endYear = Number(endYearString); | ||
|
||
assert( | ||
endYear === currentYear || | ||
endYear === (await getLatestCommitYearForFile(file)), | ||
`${file} has an invalid end license year` | ||
); | ||
}); | ||
} | ||
|
||
// Note: this check will fail if we switch to ES modules. See | ||
// <https://stackoverflow.com/a/60309682>. | ||
if (require.main === module) { | ||
main().catch(err => { | ||
// eslint-disable-next-line no-console | ||
console.error(err); | ||
process.exit(1); | ||
}); | ||
} |