Add a license header scanner 00/5600/6
authorThanh Ha <thanh.ha@linuxfoundation.org>
Wed, 19 Jul 2017 22:50:24 +0000 (18:50 -0400)
committerThanh Ha <thanh.ha@linuxfoundation.org>
Fri, 28 Jul 2017 02:15:27 +0000 (22:15 -0400)
Simple scanner to check code files for a license header. Does not care
about the exact formatting of the license header as long as all the text
exists in the correct order.

This version only supports headers using '#' as the comment string.

Issue: RELENG-279
Change-Id: Id4030f040c3de4350c59776ed21eed497e5d6f8d
Signed-off-by: Thanh Ha <thanh.ha@linuxfoundation.org>
docs/commands/index.rst
docs/commands/license.rst [new file with mode: 0644]
lftools/cli/__init__.py
lftools/cli/license.py [new file with mode: 0644]
lftools/license.py [new file with mode: 0644]
license-header.txt [new file with mode: 0644]
tests/fixtures/license/license-header.txt [new file with mode: 0644]
tests/fixtures/license/license.py [new file with mode: 0644]
tests/fixtures/license/no_license1.py [new file with mode: 0644]
tests/fixtures/license/no_license2.py [new file with mode: 0644]
tests/test_license.py [new file with mode: 0644]

index 687e59c..c4bd35b 100644 (file)
@@ -9,6 +9,7 @@ bash. It supports the following commands.
     :maxdepth: 2
 
     deploy
+    license
     nexus
     openstack
     sign
diff --git a/docs/commands/license.rst b/docs/commands/license.rst
new file mode 100644 (file)
index 0000000..f088f45
--- /dev/null
@@ -0,0 +1,16 @@
+*******
+License
+*******
+
+.. program-output:: lftools license --help
+
+Commands
+========
+
+.. contents:: License Commands
+    :local:
+
+check
+-----
+
+.. program-output:: lftools license check --help
index 823bd8d..022c5fc 100644 (file)
@@ -16,6 +16,7 @@ import click
 
 from lftools.cli.deploy import deploy
 from lftools.cli.jenkins import jenkins_cli
+from lftools.cli.license import license
 from lftools.cli.nexus import nexus
 from lftools.cli.sign import sign
 from lftools.cli.version import version
@@ -32,6 +33,7 @@ def cli(ctx):
 
 cli.add_command(deploy)
 cli.add_command(jenkins_cli, name='jenkins')
+cli.add_command(license)
 cli.add_command(nexus)
 cli.add_command(openstack)
 cli.add_command(sign)
diff --git a/lftools/cli/license.py b/lftools/cli/license.py
new file mode 100644 (file)
index 0000000..4c4d760
--- /dev/null
@@ -0,0 +1,66 @@
+# SPDX-License-Identifier: EPL-1.0
+##############################################################################
+# Copyright (c) 2017 The Linux Foundation and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Eclipse Public License v1.0
+# which accompanies this distribution, and is available at
+# http://www.eclipse.org/legal/epl-v10.html
+##############################################################################
+"""Scan code for license headers."""
+
+__author__ = 'Thanh Ha'
+
+
+import sys
+
+import click
+
+from lftools.license import check_license
+from lftools.license import check_license_directory
+
+
+@click.group()
+@click.pass_context
+def license(ctx):
+    """Scan code for license headers."""
+    pass
+
+
+@click.command()
+@click.argument('source')
+@click.option('-l', '--license', default='license-header.txt',
+              help='License header file to compare against.')
+@click.pass_context
+def check(ctx, license, source):
+    """Check files for missing license headers.
+
+    Does not care if about line formatting of the license as long as all of the
+    text is there and in the correct order.
+
+    Note: This code only supports '#' comments for license headers.
+    """
+    exit_code = check_license(license, source)
+    sys.exit(exit_code)
+
+
+@click.command(name='check-dir')
+@click.argument('directory')
+@click.option('-e', '--extension', default='py',
+              help='File extension to search for.')
+@click.option('-l', '--license', default='license-header.txt',
+              help='License header file to compare against.')
+@click.pass_context
+def check_directory(ctx, license, directory, extension):
+    """Check directory for files missing license headers.
+
+    Does not care if about line formatting of the license as long as all of the
+    text is there and in the correct order.
+
+    Note: This code only supports '#' comments for license headers.
+    """
+    check_license_directory(license, directory, extension)
+
+
+license.add_command(check)
+license.add_command(check_directory)
diff --git a/lftools/license.py b/lftools/license.py
new file mode 100644 (file)
index 0000000..93cfdaa
--- /dev/null
@@ -0,0 +1,74 @@
+# -*- coding: utf-8 -*-
+# SPDX-License-Identifier: EPL-1.0
+##############################################################################
+# Copyright (c) 2017 The Linux Foundation and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Eclipse Public License v1.0
+# which accompanies this distribution, and is available at
+# http://www.eclipse.org/legal/epl-v10.html
+##############################################################################
+"""Scans code for a valid license header."""
+
+__author__ = 'Thanh Ha'
+
+
+import os
+import re
+import sys
+
+
+def get_header_text(_file):
+    """Scan a file and pulls out the license header.
+
+    Returns a string containing the license header with newlines and copyright
+    lines stripped.
+
+    Note: This function only supports '#' comments for license headers.
+    """
+    text = ''
+    with open(_file, 'r') as data:
+        lines = data.readlines()
+        for line in lines:
+            result = re.search(r'\s*[#]', line)
+            if not result:
+                break
+            string = re.sub(r'^\s*#+', '', line).strip()
+            if bool(re.match('Copyright', string, re.I)):  # Ignore the Copyright line
+                continue
+            text += ' {}'.format(string)
+    # Strip unnecessary spacing
+    text = re.sub('\s+', ' ', text).strip()
+    return text
+
+
+def check_license(license_file, code_file):
+    """Compare a file with the provided license header.
+
+    Reports if license header is missing or does not match the text of
+    license_file.
+    """
+    license_header = get_header_text(license_file)
+    code_header = get_header_text(code_file)
+
+    if not license_header in code_header:
+        print('ERROR: {} is missing or has incorrect license header.'.format(code_file))
+        return 1
+
+    return 0
+
+
+def check_license_directory(license_file, directory, extension="py"):
+    """Search a directory for files and calls check_license()."""
+    missing_license = False
+
+    for root, dirs, files in os.walk(directory):
+        for file in files:
+            if file.endswith(".{}".format(extension)):
+                if check_license(license_file, os.path.join(root, file)):
+                    missing_license = True
+
+    if missing_license:
+        sys.exit(1)
+
+    print('Scan completed did not detect any files missing license headers.')
diff --git a/license-header.txt b/license-header.txt
new file mode 100644 (file)
index 0000000..6370a91
--- /dev/null
@@ -0,0 +1,9 @@
+# SPDX-License-Identifier: EPL-1.0
+##############################################################################
+# COPYRIGHT
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Eclipse Public License v1.0
+# which accompanies this distribution, and is available at
+# http://www.eclipse.org/legal/epl-v10.html
+##############################################################################
diff --git a/tests/fixtures/license/license-header.txt b/tests/fixtures/license/license-header.txt
new file mode 100644 (file)
index 0000000..6370a91
--- /dev/null
@@ -0,0 +1,9 @@
+# SPDX-License-Identifier: EPL-1.0
+##############################################################################
+# COPYRIGHT
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Eclipse Public License v1.0
+# which accompanies this distribution, and is available at
+# http://www.eclipse.org/legal/epl-v10.html
+##############################################################################
diff --git a/tests/fixtures/license/license.py b/tests/fixtures/license/license.py
new file mode 100644 (file)
index 0000000..0ad9dea
--- /dev/null
@@ -0,0 +1,10 @@
+# SPDX-License-Identifier: EPL-1.0
+##############################################################################
+# Copyright (c) 2017 The Linux Foundation and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Eclipse Public License v1.0
+# which accompanies this distribution, and is available at
+# http://www.eclipse.org/legal/epl-v10.html
+##############################################################################
+"""Test code file with license header."""
diff --git a/tests/fixtures/license/no_license1.py b/tests/fixtures/license/no_license1.py
new file mode 100644 (file)
index 0000000..527be5d
--- /dev/null
@@ -0,0 +1 @@
+"""Test code file without license header."""
diff --git a/tests/fixtures/license/no_license2.py b/tests/fixtures/license/no_license2.py
new file mode 100644 (file)
index 0000000..527be5d
--- /dev/null
@@ -0,0 +1 @@
+"""Test code file without license header."""
diff --git a/tests/test_license.py b/tests/test_license.py
new file mode 100644 (file)
index 0000000..08d1263
--- /dev/null
@@ -0,0 +1,60 @@
+# SPDX-License-Identifier: EPL-1.0
+##############################################################################
+# Copyright (c) 2017 The Linux Foundation and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Eclipse Public License v1.0
+# which accompanies this distribution, and is available at
+# http://www.eclipse.org/legal/epl-v10.html
+##############################################################################
+"""Test license command."""
+
+import os
+
+import pytest
+
+from lftools import cli
+
+FIXTURE_DIR = os.path.join(
+    os.path.dirname(os.path.realpath(__file__)),
+    'fixtures',
+    )
+
+
+@pytest.mark.datafiles(
+    os.path.join(FIXTURE_DIR, 'license'),
+    )
+def test_check_license(cli_runner, datafiles):
+    """Test check_license() command."""
+    os.chdir(str(datafiles))
+
+    # Check that license checker passes when file has license.
+    result = cli_runner.invoke(cli.cli, ['license', 'check', 'license.py'])
+    # noqa: B101 .
+    assert result.exit_code == 0
+
+    # Check that license checker fails when file is missing license.
+    result = cli_runner.invoke(cli.cli, ['license', 'check', 'no_license1.py'])
+    # noqa: B101 .
+    assert result.exit_code == 1
+
+
+@pytest.mark.datafiles(
+    os.path.join(FIXTURE_DIR, 'license'),
+    )
+def test_check_license_directory(cli_runner, datafiles):
+    """Test check_license_directory() command."""
+    os.chdir(str(datafiles))
+
+    # Check that check-dir fails due to directory containing files
+    # with no license.
+    result = cli_runner.invoke(cli.cli, ['license', 'check-dir', '.'])
+    # noqa: B101 .
+    assert result.exit_code == 1
+
+    # Check that check-dir passes when directory contains files with licenses
+    os.remove('no_license1.py')
+    os.remove('no_license2.py')
+    result = cli_runner.invoke(cli.cli, ['license', 'check-dir', '.'])
+    # noqa: B101 .
+    assert result.exit_code == 0