diff --git a/.boilerplate.json b/.boilerplate.json new file mode 100644 index 00000000..75aaa208 --- /dev/null +++ b/.boilerplate.json @@ -0,0 +1,11 @@ +{ + "dirs_to_skip" : [ + "vendor", + "verify/boilerplate/testdata/" + ], + "not_generated_files_to_skip" : [ + "verify/boilerplate/boilerplate.py", + "kazel/generator.go", + "tools/generate_crosstool/main.go" + ] +} diff --git a/.travis.yml b/.travis.yml index 78326b85..11464fec 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,6 +22,7 @@ install: script: # Build first since we need the generated protobuf for the govet checks - bazel build --config=ci //... + - cd verify/boilerplate && python -m unittest boilerplate_test ; cd - - ./verify/verify-boilerplate.sh --rootdir="$(pwd)" -v - GOPATH="${GOPATH}:$(pwd)/bazel-bin/verify/verify-go-src-go_path" ./verify/verify-go-src.sh --rootdir "$(pwd)" -v - ./verify/verify-bazel.sh diff --git a/verify/README.md b/verify/README.md index 6eca4ece..1147a8af 100644 --- a/verify/README.md +++ b/verify/README.md @@ -33,9 +33,45 @@ script: # - vendor/github.com/kubernetes/repo-infra/verify-go-src.sh --rootdir=$(pwd) -v ``` -## Verify boilerplate +## Verify & Ensure boilerplate + +- `verify-boilerplate.sh`: + Verifies that the boilerplate for various formats (go files, Makefile, etc.) + is included in each file. +- `ensure-boilerplate.sh`: + Ensure that various formats (see above) have the boilerplate included. + +The scripts assume the root of the repo to be two levels up of the directory +the scripts are in. + +If this is not the case, you can configure the root of the reop by either +setting `REPO_ROOT` or by calling the scripts with `--root-dir=`. + +You can put a config file into the root of your repo named `boilerplate.json`. +The config can look something like this: +```json +{ + "dirs_to_skip" : [ + "vendor", + "tools/contrib" + ], + "not_generated_files_to_skip" : [ + "some/file", + "some/other/file.something" + ] +} +``` +Currently supported settings are +- `dirs_to_skip` + A list of directories which is excluded when checking or adding the headers +- `not_generated_files_to_skip` + A list of all the files contain 'DO NOT EDIT', but are not generated + +All other settings will be ignored. + +### Tests -Verifies that the boilerplate for various formats (go files, Makefile, etc.) is included in each file: `verify-boilerplate.sh`. +To run the test, cd into the boilerplate directory and run `python -m unittest boilerplate_test`. ## Verify go source code diff --git a/verify/boilerplate/boilerplate.generatego.txt b/verify/boilerplate/boilerplate.generatego.txt new file mode 100644 index 00000000..b7c650da --- /dev/null +++ b/verify/boilerplate/boilerplate.generatego.txt @@ -0,0 +1,16 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + diff --git a/verify/boilerplate/boilerplate.py b/verify/boilerplate/boilerplate.py index 3507c214..75a5290d 100755 --- a/verify/boilerplate/boilerplate.py +++ b/verify/boilerplate/boilerplate.py @@ -17,14 +17,13 @@ from __future__ import print_function import argparse +import datetime import difflib import glob -import json -import mmap import os import re import sys -from datetime import date +import json parser = argparse.ArgumentParser() parser.add_argument( @@ -47,10 +46,21 @@ help="give verbose output regarding why a file does not pass", action="store_true") +parser.add_argument( + "--ensure", + help="ensure all files which should have appropriate licence headers have them prepended", + action="store_true") + args = parser.parse_args() verbose_out = sys.stderr if args.verbose else open("/dev/null", "w") +default_skipped_dirs = ['Godeps', '.git', 'vendor'] + +# list all the files that contain 'DO NOT EDIT', but are not generated +default_skipped_not_generated = [] + + def get_refs(): refs = {} @@ -64,7 +74,38 @@ def get_refs(): return refs -def file_passes(filename, refs, regexs): + +def is_generated_file(filename, data, regexs, files_to_skip): + for d in files_to_skip: + if d in filename: + return False + + p = regexs["generated"] + return p.search(data) + + +def match_and_delete(content, re): + match = re.search(content) + if match is None: + return content, None + return re.sub("", content, 1), match.group() + + +def replace_specials(content, extension, regexs): + # remove build tags from the top of Go files + if extension == "go" or extension == "generatego": + re = regexs["go_build_constraints"] + return match_and_delete(content, re) + + # remove shebang from the top of shell files + if extension == "sh": + re = regexs["shebang"] + return match_and_delete(content, re) + + return content, None + + +def file_passes(filename, refs, regexs, not_generated_files_to_skip): try: f = open(filename, 'r') except Exception as exc: @@ -74,22 +115,17 @@ def file_passes(filename, refs, regexs): data = f.read() f.close() - basename = os.path.basename(filename) - extension = file_extension(filename) - if extension != "": - ref = refs[extension] - else: - ref = refs[basename] + ref, extension, generated = analyze_file( + filename, data, refs, regexs, not_generated_files_to_skip) - # remove build tags from the top of Go files - if extension == "go": - p = regexs["go_build_constraints"] - (data, found) = p.subn("", data, 1) + return file_content_passes(data, filename, ref, extension, generated, regexs) - # remove shebang from the top of shell files - if extension == "sh" or extension == "py": - p = regexs["shebang"] - (data, found) = p.subn("", data, 1) + +def file_content_passes(data, filename, ref, extension, generated, regexs): + if ref is None: + return True + + data, _ = replace_specials(data, extension, regexs) data = data.splitlines() @@ -106,15 +142,19 @@ def file_passes(filename, refs, regexs): p = regexs["year"] for d in data: if p.search(d): - print('File %s is missing the year' % filename, file=verbose_out) + if generated: + print('File %s has the YEAR field, but it should not be in generated file' % filename, file=verbose_out) + else: + print('File %s has the YEAR field, but missing the year of date' % filename, file=verbose_out) return False - # Replace all occurrences of the regex "CURRENT_YEAR|...|2016|2015|2014" with "YEAR" - p = regexs["date"] - for i, d in enumerate(data): - (data[i], found) = p.subn('YEAR', d) - if found != 0: - break + if not generated: + # Replace all occurrences of the regex "2014|2015|2016|2017|2018" with "YEAR" + p = regexs["date"] + for i, d in enumerate(data): + (data[i], found) = p.subn('YEAR', d) + if found != 0: + break # if we don't match the reference at this point, fail if ref != data: @@ -128,17 +168,25 @@ def file_passes(filename, refs, regexs): return True + def file_extension(filename): return os.path.splitext(filename)[1].split(".")[-1].lower() -skipped_dirs = ['Godeps', 'third_party', '_gopath', '_output', '.git', - 'cluster/env.sh', 'vendor', 'test/e2e/generated/bindata.go', - 'repo-infra/verify/boilerplate/test', '.glide'] -def normalize_files(files): +def read_config_file(conf_path): + try: + with open(conf_path) as json_data_file: + return json.load(json_data_file) + except ValueError: + raise + except: + return {'dirs_to_skip': default_skipped_dirs, 'not_generated_files_to_skip': default_skipped_not_generated} + + +def normalize_files(files, dirs_to_skip): newfiles = [] for pathname in files: - if any(x in pathname for x in skipped_dirs): + if any(x in pathname for x in dirs_to_skip): continue newfiles.append(pathname) for i, pathname in enumerate(newfiles): @@ -146,7 +194,8 @@ def normalize_files(files): newfiles[i] = os.path.join(args.rootdir, pathname) return newfiles -def get_files(extensions): + +def get_files(extensions, dirs_to_skip): files = [] if len(args.filenames) > 0: files = args.filenames @@ -156,7 +205,7 @@ def get_files(extensions): # as we would prune these later in normalize_files(). But doing it # cuts down the amount of filesystem walking we do and cuts down # the size of the file list - for d in skipped_dirs: + for d in dirs_to_skip: if d in dirs: dirs.remove(d) @@ -164,8 +213,7 @@ def get_files(extensions): pathname = os.path.join(root, name) files.append(pathname) - files = normalize_files(files) - + files = normalize_files(files, dirs_to_skip) outfiles = [] for pathname in files: basename = os.path.basename(pathname) @@ -174,29 +222,101 @@ def get_files(extensions): outfiles.append(pathname) return outfiles + +def analyze_file(file_name, file_content, refs, regexs, not_generated_files_to_skip): + # determine if the file is automatically generated + generated = is_generated_file( + file_name, file_content, regexs, not_generated_files_to_skip) + + base_name = os.path.basename(file_name) + if generated: + extension = "generatego" + else: + extension = file_extension(file_name) + + if extension != "": + ref = refs[extension] + else: + ref = refs.get(base_name, None) + + return ref, extension, generated + + +def ensure_boilerplate_file(file_name, refs, regexs, not_generated_files_to_skip): + with open(file_name, mode='r+') as f: + file_content = f.read() + + ref, extension, generated = analyze_file( + file_name, file_content, refs, regexs, not_generated_files_to_skip) + + # licence header + licence_header = os.linesep.join(ref) + + # content without shebang and such + content_without_specials, special_header = replace_specials( + file_content, extension, regexs) + + # new content, to be writen to the file + new_content = '' + + # shebang and such + if special_header is not None: + new_content += special_header + + # licence header + current_year = str(datetime.datetime.now().year) + year_replacer = regexs['year'] + new_content += year_replacer.sub(current_year, licence_header, 1) + + # actual content + new_content += os.linesep + content_without_specials + + f.seek(0) + f.write(new_content) + + +def get_dates(): + years = datetime.datetime.now().year + return '(%s)' % '|'.join((str(year) for year in range(2014, years+1))) + + def get_regexs(): regexs = {} # Search for "YEAR" which exists in the boilerplate, but shouldn't in the real thing - regexs["year"] = re.compile( 'YEAR' ) - # dates can be 2014, 2015, 2016, ..., CURRENT_YEAR, company holder names can be anything - years = range(2014, date.today().year + 1) - regexs["date"] = re.compile( '(%s)' % "|".join(map(lambda l: str(l), years)) ) + regexs["year"] = re.compile('YEAR') + # get_dates return 2014, 2015, 2016, 2017, or 2018 until the current year as a regex like: "(2014|2015|2016|2017|2018)"; + # company holder names can be anything + regexs["date"] = re.compile(get_dates()) # strip // +build \n\n build constraints - regexs["go_build_constraints"] = re.compile(r"^(// \+build.*\n)+\n", re.MULTILINE) + regexs["go_build_constraints"] = re.compile( + r"^(// \+build.*\n)+\n", re.MULTILINE) # strip #!.* from shell scripts regexs["shebang"] = re.compile(r"^(#!.*\n)\n*", re.MULTILINE) + # Search for generated files + regexs["generated"] = re.compile('DO NOT EDIT') return regexs + def main(): + config_file_path = os.path.join(args.rootdir, ".boilerplate.json") + config = read_config_file(config_file_path) + regexs = get_regexs() refs = get_refs() - filenames = get_files(refs.keys()) + filenames = get_files(refs.keys(), config.get('dirs_to_skip')) + not_generated_files_to_skip = config.get('not_generated_files_to_skip', []) for filename in filenames: - if not file_passes(filename, refs, regexs): - print(filename, file=sys.stdout) + if not file_passes(filename, refs, regexs, not_generated_files_to_skip): + if args.ensure: + print("adding boilerplate header to %s" % filename) + ensure_boilerplate_file( + filename, refs, regexs, not_generated_files_to_skip) + else: + print(filename, file=sys.stdout) return 0 + if __name__ == "__main__": - sys.exit(main()) + sys.exit(main()) diff --git a/verify/boilerplate/boilerplate.py.txt b/verify/boilerplate/boilerplate.py.txt index 384f325a..a2e72e59 100644 --- a/verify/boilerplate/boilerplate.py.txt +++ b/verify/boilerplate/boilerplate.py.txt @@ -1,3 +1,5 @@ +#!/usr/bin/env python + # Copyright YEAR The Kubernetes Authors. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/verify/boilerplate/boilerplate_test.py b/verify/boilerplate/boilerplate_test.py index b8d5b8e9..e4d69976 100644 --- a/verify/boilerplate/boilerplate_test.py +++ b/verify/boilerplate/boilerplate_test.py @@ -14,39 +14,195 @@ # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import print_function import boilerplate import unittest import StringIO import os import sys +import tempfile +import re +from contextlib import contextmanager -class TestBoilerplate(unittest.TestCase): - """ - Note: run this test from the hack/boilerplate directory. - - $ python -m unittest boilerplate_test - """ +base_dir = os.getcwd() - def test_boilerplate(self): - os.chdir("test/") - class Args(object): - def __init__(self): +class DefaultArgs(object): + def __init__(self): self.filenames = [] self.rootdir = "." - self.boilerplate_dir = "../" + self.boilerplate_dir = base_dir self.verbose = True + self.ensure = False + + +class TestBoilerplate(unittest.TestCase): + """ + Note: run this test from the inside the boilerplate directory. + + $ python -m unittest boilerplate_test + """ + + def setUp(self): + os.chdir(base_dir) + boilerplate.args = DefaultArgs() + + def test_boilerplate(self): + os.chdir("testdata/default/") + + # capture stdout + old_stdout = sys.stdout + sys.stdout = StringIO.StringIO() + + ret = boilerplate.main() + + output = sorted(sys.stdout.getvalue().split()) + + sys.stdout = old_stdout + + self.assertItemsEqual( + output, ['././fail.go', '././fail.py', '././fail.sh']) + + def test_read_config(self): + config_file = "./testdata/with_config/.boilerplate.json" + config = boilerplate.read_config_file(config_file) + self.assertItemsEqual(config.get('dirs_to_skip'), [ + 'dir_to_skip', 'dont_want_this', 'not_interested', '.']) + self.assertItemsEqual(config.get('not_generated_files_to_skip'), [ + 'alice skips a file', 'bob skips another file']) + + def test_read_nonexistent_config(self): + config_file = '/nonexistent' + config = boilerplate.read_config_file(config_file) + self.assertItemsEqual(config['dirs_to_skip'], + boilerplate.default_skipped_dirs) + self.assertItemsEqual(config['not_generated_files_to_skip'], + boilerplate.default_skipped_not_generated) + + def test_read_malformed_config(self): + config_file = './testdata/with_config/.boilerplate.bad.json' + with self.assertRaises(Exception): + boilerplate.read_config_file(config_file) + + def test_read_config_called_with_correct_path(self): + boilerplate.args.rootdir = "/tmp/some/path" + with function_mocker('read_config_file', boilerplate, return_value={}) as mock_args: + boilerplate.main() + self.assertEqual(len(mock_args), 1) + self.assertEqual( + mock_args[0][0], "/tmp/some/path/.boilerplate.json") + + def test_get_files_with_skipping_dirs(self): + refs = boilerplate.get_refs() + skip_dirs = ['.'] + files = boilerplate.get_files(refs, skip_dirs) + + self.assertEqual(files, []) + + def test_get_files_with_skipping_not_generated_files(self): + refs = boilerplate.get_refs() + regexes = boilerplate.get_regexs() + files_to_skip = ['boilerplate.py'] + filename = 'boilerplate.py' + + passes = boilerplate.file_passes( + filename, refs, regexes, files_to_skip) + + self.assertEqual(passes, True) + + def test_ignore_when_no_valid_boilerplate_template(self): + with tempfile.NamedTemporaryFile() as temp_file_to_check: + passes = boilerplate.file_passes( + temp_file_to_check.name, boilerplate.get_refs(), boilerplate.get_regexs(), []) + self.assertEqual(passes, True) + + def test_add_boilerplate_to_file(self): + with tmp_copy("./testdata/default/fail.sh", suffix='.sh') as tmp_file_name: + boilerplate.ensure_boilerplate_file( + tmp_file_name, boilerplate.get_refs(), boilerplate.get_regexs(), [] + ) + + passes = boilerplate.file_passes( + tmp_file_name, boilerplate.get_refs(), boilerplate.get_regexs(), []) + self.assertEqual(passes, True) + + with open(tmp_file_name) as x: + first_line = x.read().splitlines()[0] + self.assertEqual(first_line, '#!/usr/bin/env bash') + + def test_replace_specials(self): + extension = "sh" + regexs = boilerplate.get_regexs() + + original_content = "\n".join([ + "#!/usr/bin/env bash", + "", + "something something", + "#!/usr/bin/env bash", + ]) + expected_content = "\n".join([ + "something something", + "#!/usr/bin/env bash", + ]) + expected_match = "\n".join([ + "#!/usr/bin/env bash", + "\n", + ]) + + actual_content, actual_match = boilerplate.replace_specials( + original_content, extension, regexs + ) + + self.assertEquals(actual_content, expected_content) + self.assertEquals(actual_match, expected_match) + + def test_ensure_command_line_flag(self): + os.chdir("./testdata/default/") + boilerplate.args.ensure = True + + with function_mocker('ensure_boilerplate_file', boilerplate) as mock_args: + boilerplate.main() + changed_files = list(map(lambda x: x[0], mock_args)) + + self.assertItemsEqual(changed_files, [ + "././fail.sh", + "././fail.py", + "././fail.go", + ]) + + +@contextmanager +def tmp_copy(file_org, suffix=None): + file_copy_fd, file_copy = tempfile.mkstemp(suffix) + + with open(file_org) as org: + os.write(file_copy_fd, org.read()) + os.close(file_copy_fd) + + yield file_copy + + os.unlink(file_copy) + + +@contextmanager +def function_mocker(function_name, original_holder, return_value=None): + # save original function implementation + original_implementation = getattr(original_holder, function_name) - # capture stdout - old_stdout = sys.stdout - sys.stdout = StringIO.StringIO() + # keep track of the args + mock_call_args = [] - boilerplate.args = Args() - ret = boilerplate.main() + # mock the function + def the_mock(*args): + mock_call_args.append(args) + if return_value is not None: + return return_value - output = sorted(sys.stdout.getvalue().split()) + # use the mock in place of the original implementation + setattr(original_holder, function_name, the_mock) - sys.stdout = old_stdout + # run + yield mock_call_args - self.assertEquals( - output, ['././fail.go', '././fail.py']) + # reset the original implementation + setattr(original_holder, function_name, original_implementation) diff --git a/verify/boilerplate/test/BUILD.bazel b/verify/boilerplate/testdata/default/BUILD.bazel similarity index 76% rename from verify/boilerplate/test/BUILD.bazel rename to verify/boilerplate/testdata/default/BUILD.bazel index ec18049a..437e02b3 100644 --- a/verify/boilerplate/test/BUILD.bazel +++ b/verify/boilerplate/testdata/default/BUILD.bazel @@ -13,5 +13,5 @@ go_library( "fail.go", "pass.go", ], - importpath = "k8s.io/repo-infra/verify/boilerplate/test", + importpath = "k8s.io/repo-infra/verify/boilerplate/testdata/default", ) diff --git a/verify/boilerplate/test/fail.go b/verify/boilerplate/testdata/default/fail.go similarity index 100% rename from verify/boilerplate/test/fail.go rename to verify/boilerplate/testdata/default/fail.go diff --git a/verify/boilerplate/test/fail.py b/verify/boilerplate/testdata/default/fail.py similarity index 100% rename from verify/boilerplate/test/fail.py rename to verify/boilerplate/testdata/default/fail.py diff --git a/verify/boilerplate/testdata/default/fail.sh b/verify/boilerplate/testdata/default/fail.sh new file mode 100644 index 00000000..d9b5f982 --- /dev/null +++ b/verify/boilerplate/testdata/default/fail.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +## invalid header + +exit_code=42 +exit $exit_code diff --git a/verify/boilerplate/test/pass.go b/verify/boilerplate/testdata/default/pass.go similarity index 100% rename from verify/boilerplate/test/pass.go rename to verify/boilerplate/testdata/default/pass.go diff --git a/verify/boilerplate/test/pass.py b/verify/boilerplate/testdata/default/pass.py similarity index 100% rename from verify/boilerplate/test/pass.py rename to verify/boilerplate/testdata/default/pass.py diff --git a/verify/boilerplate/testdata/with_config/.boilerplate.bad.json b/verify/boilerplate/testdata/with_config/.boilerplate.bad.json new file mode 100644 index 00000000..05202d14 --- /dev/null +++ b/verify/boilerplate/testdata/with_config/.boilerplate.bad.json @@ -0,0 +1,3 @@ +{ + "dirs_to_skip": [ +} diff --git a/verify/boilerplate/testdata/with_config/.boilerplate.json b/verify/boilerplate/testdata/with_config/.boilerplate.json new file mode 100644 index 00000000..afec23eb --- /dev/null +++ b/verify/boilerplate/testdata/with_config/.boilerplate.json @@ -0,0 +1,12 @@ +{ + "dirs_to_skip": [ + "dir_to_skip", + "dont_want_this", + "not_interested", + "." + ], + "not_generated_files_to_skip": [ + "alice skips a file", + "bob skips another file" + ] +} diff --git a/verify/boilerplate/testdata/with_config/BUILD.bazel b/verify/boilerplate/testdata/with_config/BUILD.bazel new file mode 100644 index 00000000..e979282c --- /dev/null +++ b/verify/boilerplate/testdata/with_config/BUILD.bazel @@ -0,0 +1,11 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = [ + "fail.go", + "pass.go", + ], + importpath = "k8s.io/repo-infra/verify/boilerplate/testdata/with_config", + visibility = ["//visibility:public"], +) diff --git a/verify/boilerplate/testdata/with_config/fail.go b/verify/boilerplate/testdata/with_config/fail.go new file mode 100644 index 00000000..16159c5a --- /dev/null +++ b/verify/boilerplate/testdata/with_config/fail.go @@ -0,0 +1,19 @@ +/* +Copyright 2014 The Kubernetes Authors. + +fail + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package test diff --git a/verify/boilerplate/testdata/with_config/fail.py b/verify/boilerplate/testdata/with_config/fail.py new file mode 100644 index 00000000..cbdd06ff --- /dev/null +++ b/verify/boilerplate/testdata/with_config/fail.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python + +# Copyright 2015 The Kubernetes Authors. +# +# failed +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/verify/boilerplate/testdata/with_config/pass.go b/verify/boilerplate/testdata/with_config/pass.go new file mode 100644 index 00000000..7508448a --- /dev/null +++ b/verify/boilerplate/testdata/with_config/pass.go @@ -0,0 +1,17 @@ +/* +Copyright 2014 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package test diff --git a/verify/boilerplate/testdata/with_config/pass.py b/verify/boilerplate/testdata/with_config/pass.py new file mode 100644 index 00000000..5b7ce29a --- /dev/null +++ b/verify/boilerplate/testdata/with_config/pass.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python + +# Copyright 2015 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +True diff --git a/verify/ensure-boilerplate.sh b/verify/ensure-boilerplate.sh new file mode 120000 index 00000000..7adc42e1 --- /dev/null +++ b/verify/ensure-boilerplate.sh @@ -0,0 +1 @@ +verify-boilerplate.sh \ No newline at end of file diff --git a/verify/verify-boilerplate.sh b/verify/verify-boilerplate.sh index 0168d336..3aaa7764 100755 --- a/verify/verify-boilerplate.sh +++ b/verify/verify-boilerplate.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # Copyright 2014 The Kubernetes Authors. # @@ -18,39 +18,61 @@ set -o errexit set -o nounset set -o pipefail -# This script is intended to be used via subtree in a top-level directory: -# / -# repo-infra/ -# verify/ +if [ "$(uname)" = 'Darwin' ]; then + readlinkf(){ perl -MCwd -e 'print Cwd::abs_path shift' "$1";} +else + readlinkf(){ readlink -f "$1"; } +fi + +# shellcheck disable=SC2128 +SCRIPT_DIR="$(cd "$(dirname "$(readlinkf "$BASH_SOURCE")")" ; pwd)" -REPO_ROOT=$(dirname "${BASH_SOURCE}")/../.. +# We assume the link to the script ( {ensure,verify}-boilerplate.sh ) to be +# in a directory 2 levels down from the repo root, e.g. in +# /repo-infra/verify/verify-boilerplate.sh +# Alternatively, you can set the project root by setting the variable +# `REPO_ROOT`. +# +# shellcheck disable=SC2128 +: "${REPO_ROOT:="$(cd "${SCRIPT_DIR}/../.." ; pwd)"}" -boilerDir="${REPO_ROOT}/repo-infra/verify/boilerplate" +boilerDir="${SCRIPT_DIR}/boilerplate/" boiler="${boilerDir}/boilerplate.py" -files_need_boilerplate=($(${boiler} "$@")) +verify() { + # shellcheck disable=SC2207 + files_need_boilerplate=( + $( "$boiler" --rootdir="$REPO_ROOT" --boilerplate-dir="$boilerDir" "$@") + ) + + # Run boilerplate check + if [[ ${#files_need_boilerplate[@]} -gt 0 ]]; then + for file in "${files_need_boilerplate[@]}"; do + echo "Boilerplate header is wrong for: ${file}" >&2 + done -# Run boilerplate.py unit tests -unitTestOut="$(mktemp)" -trap cleanup EXIT -cleanup() { - rm "${unitTestOut}" + return 1 + fi } -pushd "${boilerDir}" >/dev/null -if ! python -m unittest boilerplate_test 2>"${unitTestOut}"; then - echo "boilerplate_test.py failed" - echo - cat "${unitTestOut}" - exit 1 -fi -popd >/dev/null +ensure() { + "$boiler" --rootdir="$REPO_ROOT" --boilerplate-dir="$boilerDir" --ensure "$@" +} -# Run boilerplate check -if [[ ${#files_need_boilerplate[@]} -gt 0 ]]; then - for file in "${files_need_boilerplate[@]}"; do - echo "Boilerplate header is wrong for: ${file}" - done +case "$0" in + */ensure-boilerplate.sh) + ensure "$@" + ;; + */verify-boilerplate.sh) + verify "$@" + ;; + *) + { + echo "unknown command '$0'" + echo "" + echo "Call the script as either 'verify-boilerplate.sh' or 'ensure-boilerplate.sh'" + } >&2 - exit 1 -fi + exit 1 + ;; +esac