#!/usr/bin/python3 # -*- coding: utf-8 -*- # # Copyright © 2018 Endless Mobile, Inc. # # SPDX-License-Identifier: LGPL-2.1-or-later # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301 USA """Integration tests for glib-mkenums utility.""" import collections import os import shutil import subprocess import sys import tempfile import textwrap import unittest import taptestrunner Result = collections.namedtuple("Result", ("info", "out", "err", "subs")) class TestMkenums(unittest.TestCase): """Integration test for running glib-mkenums. This can be run when installed or uninstalled. When uninstalled, it requires G_TEST_BUILDDIR and G_TEST_SRCDIR to be set. The idea with this test harness is to test the glib-mkenums utility, its handling of command line arguments, its exit statuses, and its handling of various C source codes. In future we could split the core glib-mkenums parsing and generation code out into a library and unit test that, and convert this test to just check command line behaviour. """ # Track the cwd, we want to back out to that to clean up our tempdir cwd = "" rspfile = False def setUp(self): self.timeout_seconds = 10 # seconds per test self.tmpdir = tempfile.TemporaryDirectory() self.cwd = os.getcwd() os.chdir(self.tmpdir.name) print("tmpdir:", self.tmpdir.name) if "G_TEST_BUILDDIR" in os.environ: self.__mkenums = os.path.join( os.environ["G_TEST_BUILDDIR"], "..", "glib-mkenums" ) else: self.__mkenums = shutil.which("glib-mkenums") print("rspfile: {}, mkenums:".format(self.rspfile), self.__mkenums) def tearDown(self): os.chdir(self.cwd) self.tmpdir.cleanup() def _write_rspfile(self, argv): import shlex with tempfile.NamedTemporaryFile( dir=self.tmpdir.name, mode="w", delete=False ) as f: contents = " ".join([shlex.quote(arg) for arg in argv]) print("Response file contains:", contents) f.write(contents) f.flush() return f.name def runMkenums(self, *args): if self.rspfile: rspfile = self._write_rspfile(args) args = ["@" + rspfile] argv = [self.__mkenums] # shebang lines are not supported on native # Windows consoles if os.name == "nt": argv.insert(0, sys.executable) argv.extend(args) print("Running:", argv) env = os.environ.copy() env["LC_ALL"] = "C.UTF-8" env["G_DEBUG"] = "fatal-warnings" print("Environment:", env) # We want to ensure consistent line endings... info = subprocess.run( argv, timeout=self.timeout_seconds, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, universal_newlines=True, ) info.check_returncode() out = info.stdout.strip() err = info.stderr.strip() # Known substitutions for standard boilerplate subs = { "standard_top_comment": "This file is generated by glib-mkenums, do not modify " "it. This code is licensed under the same license as the " "containing project. Note that it links to GLib, so must " "comply with the LGPL linking clauses.", "standard_bottom_comment": "Generated data ends here", } result = Result(info, out, err, subs) print("Output:", result.out) return result def runMkenumsWithTemplate(self, template_contents, *args): with tempfile.NamedTemporaryFile( dir=self.tmpdir.name, suffix=".template", delete=False ) as template_file: # Write out the template. template_file.write(template_contents.encode("utf-8")) print(template_file.name + ":", template_contents) template_file.flush() return self.runMkenums("--template", template_file.name, *args) def runMkenumsWithAllSubstitutions(self, *args): """Run glib-mkenums with a template which outputs all substitutions.""" template_contents = """ /*** BEGIN file-header ***/ file-header /*** END file-header ***/ /*** BEGIN file-production ***/ file-production filename: @filename@ basename: @basename@ /*** END file-production ***/ /*** BEGIN enumeration-production ***/ enumeration-production EnumName: @EnumName@ enum_name: @enum_name@ ENUMNAME: @ENUMNAME@ ENUMSHORT: @ENUMSHORT@ ENUMPREFIX: @ENUMPREFIX@ enumsince: @enumsince@ type: @type@ Type: @Type@ TYPE: @TYPE@ /*** END enumeration-production ***/ /*** BEGIN value-header ***/ value-header EnumName: @EnumName@ enum_name: @enum_name@ ENUMNAME: @ENUMNAME@ ENUMSHORT: @ENUMSHORT@ ENUMPREFIX: @ENUMPREFIX@ enumsince: @enumsince@ type: @type@ Type: @Type@ TYPE: @TYPE@ /*** END value-header ***/ /*** BEGIN value-production ***/ value-production VALUENAME: @VALUENAME@ valuenick: @valuenick@ valuenum: @valuenum@ type: @type@ Type: @Type@ TYPE: @TYPE@ /*** END value-production ***/ /*** BEGIN value-tail ***/ value-tail EnumName: @EnumName@ enum_name: @enum_name@ ENUMNAME: @ENUMNAME@ ENUMSHORT: @ENUMSHORT@ ENUMPREFIX: @ENUMPREFIX@ enumsince: @enumsince@ type: @type@ Type: @Type@ TYPE: @TYPE@ /*** END value-tail ***/ /*** BEGIN comment ***/ comment comment: @comment@ /*** END comment ***/ /*** BEGIN file-tail ***/ file-tail /*** END file-tail ***/ """ return self.runMkenumsWithTemplate(template_contents, *args) def runMkenumsWithHeader(self, h_contents, encoding="utf-8"): with tempfile.NamedTemporaryFile( dir=self.tmpdir.name, suffix=".h", delete=False ) as h_file: # Write out the header to be scanned. h_file.write(h_contents.encode(encoding)) print(h_file.name + ":", h_contents) h_file.flush() # Run glib-mkenums with a template which outputs all substitutions. result = self.runMkenumsWithAllSubstitutions(h_file.name) # Known substitutions for generated filenames. result.subs.update( {"filename": h_file.name, "basename": os.path.basename(h_file.name)} ) return result def assertSingleEnum( self, result, enum_name_camel, enum_name_lower, enum_name_upper, enum_name_short, enum_prefix, enum_since, type_lower, type_camel, type_upper, value_name, value_nick, value_num, ): """Assert that out (from runMkenumsWithHeader()) contains a single enum and value matching the given arguments.""" subs = dict( { "enum_name_camel": enum_name_camel, "enum_name_lower": enum_name_lower, "enum_name_upper": enum_name_upper, "enum_name_short": enum_name_short, "enum_prefix": enum_prefix, "enum_since": enum_since, "type_lower": type_lower, "type_camel": type_camel, "type_upper": type_upper, "value_name": value_name, "value_nick": value_nick, "value_num": value_num, }, **result.subs ) self.assertEqual( """ comment comment: {standard_top_comment} file-header file-production filename: {filename} basename: {basename} enumeration-production EnumName: {enum_name_camel} enum_name: {enum_name_lower} ENUMNAME: {enum_name_upper} ENUMSHORT: {enum_name_short} ENUMPREFIX: {enum_prefix} enumsince: {enum_since} type: {type_lower} Type: {type_camel} TYPE: {type_upper} value-header EnumName: {enum_name_camel} enum_name: {enum_name_lower} ENUMNAME: {enum_name_upper} ENUMSHORT: {enum_name_short} ENUMPREFIX: {enum_prefix} enumsince: {enum_since} type: {type_lower} Type: {type_camel} TYPE: {type_upper} value-production VALUENAME: {value_name} valuenick: {value_nick} valuenum: {value_num} type: {type_lower} Type: {type_camel} TYPE: {type_upper} value-tail EnumName: {enum_name_camel} enum_name: {enum_name_lower} ENUMNAME: {enum_name_upper} ENUMSHORT: {enum_name_short} ENUMPREFIX: {enum_prefix} enumsince: {enum_since} type: {type_lower} Type: {type_camel} TYPE: {type_upper} file-tail comment comment: {standard_bottom_comment} """.format( **subs ).strip(), result.out, ) def test_help(self): """Test the --help argument.""" result = self.runMkenums("--help") self.assertIn("usage: glib-mkenums", result.out) def test_no_args(self): """Test running with no arguments at all.""" result = self.runMkenums() self.assertEqual("", result.err) self.assertEqual( """/* {standard_top_comment} */ /* {standard_bottom_comment} */""".format( **result.subs ), result.out.strip(), ) def test_empty_template(self): """Test running with an empty template and no header files.""" result = self.runMkenumsWithTemplate("") self.assertEqual("", result.err) self.assertEqual( """/* {standard_top_comment} */ /* {standard_bottom_comment} */""".format( **result.subs ), result.out.strip(), ) def test_no_headers(self): """Test running with a complete template, but no header files.""" result = self.runMkenumsWithAllSubstitutions() self.assertEqual("", result.err) self.assertEqual( """ comment comment: {standard_top_comment} file-header file-tail comment comment: {standard_bottom_comment} """.format( **result.subs ).strip(), result.out, ) def test_empty_header(self): """Test an empty header.""" result = self.runMkenumsWithHeader("") self.assertEqual("", result.err) self.assertEqual( """ comment comment: {standard_top_comment} file-header file-tail comment comment: {standard_bottom_comment} """.format( **result.subs ).strip(), result.out, ) def test_enum_name(self): """Test typedefs with an enum and a typedef name. Bug #794506.""" h_contents = """ typedef enum _SomeEnumIdentifier { ENUM_VALUE } SomeEnumIdentifier; """ result = self.runMkenumsWithHeader(h_contents) self.assertEqual("", result.err) self.assertSingleEnum( result, "SomeEnumIdentifier", "some_enum_identifier", "SOME_ENUM_IDENTIFIER", "ENUM_IDENTIFIER", "SOME", "", "enum", "Enum", "ENUM", "ENUM_VALUE", "value", "0", ) def test_non_utf8_encoding(self): """Test source files with non-UTF-8 encoding. Bug #785113.""" h_contents = """ /* Copyright © La Peña */ typedef enum { ENUM_VALUE } SomeEnumIdentifier; """ result = self.runMkenumsWithHeader(h_contents, encoding="iso-8859-1") self.assertIn("WARNING: UnicodeWarning: ", result.err) self.assertSingleEnum( result, "SomeEnumIdentifier", "some_enum_identifier", "SOME_ENUM_IDENTIFIER", "ENUM_IDENTIFIER", "SOME", "", "enum", "Enum", "ENUM", "ENUM_VALUE", "value", "0", ) def test_reproducible(self): """Test builds are reproducible regardless of file ordering. Bug #691436.""" template_contents = "template" h_contents1 = """ typedef enum { FIRST, } Header1; """ h_contents2 = """ typedef enum { SECOND, } Header2; """ with tempfile.NamedTemporaryFile( dir=self.tmpdir.name, suffix="1.h", delete=False ) as h_file1, tempfile.NamedTemporaryFile( dir=self.tmpdir.name, suffix="2.h", delete=False ) as h_file2: # Write out the headers. h_file1.write(h_contents1.encode("utf-8")) h_file2.write(h_contents2.encode("utf-8")) h_file1.flush() h_file2.flush() # Run glib-mkenums with the headers in one order, and then again # in another order. result1 = self.runMkenumsWithTemplate( template_contents, h_file1.name, h_file2.name ) self.assertEqual("", result1.err) result2 = self.runMkenumsWithTemplate( template_contents, h_file2.name, h_file1.name ) self.assertEqual("", result2.err) # The output should be the same. self.assertEqual(result1.out, result2.out) def test_no_nick(self): """Test trigraphs with a desc but no nick. Issue #1360.""" h_contents = """ typedef enum { GEGL_SAMPLER_NEAREST = 0, /*< desc="nearest" >*/ } GeglSamplerType; """ result = self.runMkenumsWithHeader(h_contents) self.assertEqual("", result.err) self.assertSingleEnum( result, "GeglSamplerType", "gegl_sampler_type", "GEGL_SAMPLER_TYPE", "SAMPLER_TYPE", "GEGL", "", "enum", "Enum", "ENUM", "GEGL_SAMPLER_NEAREST", "nearest", "0", ) def test_with_double_quotes(self): """Test trigraphs with double-quoted expressions. Issue #65.""" h_contents = """ typedef enum { FOO_VALUE /*< nick="eek, a comma" >*/ } Foo; """ result = self.runMkenumsWithHeader(h_contents) self.assertEqual("", result.err) self.assertSingleEnum( result, "Foo", "foo_", "FOO_", "", "FOO", "", "enum", "Enum", "ENUM", "FOO_VALUE", "eek, a comma", "0", ) def test_filename_basename_in_fhead_ftail(self): template_contents = """ /*** BEGIN file-header ***/ file-header filename: @filename@ basename: @basename@ /*** END file-header ***/ /*** BEGIN comment ***/ comment comment: @comment@ /*** END comment ***/ /*** BEGIN file-tail ***/ file-tail filename: @filename@ basename: @basename@ /*** END file-tail ***/""" result = self.runMkenumsWithTemplate(template_contents) self.assertEqual( textwrap.dedent( """ WARNING: @filename@ used in file-header section. WARNING: @basename@ used in file-header section. WARNING: @filename@ used in file-tail section. WARNING: @basename@ used in file-tail section. """ ).strip(), result.err, ) self.assertEqual( """ comment comment: {standard_top_comment} file-header filename: @filename@ basename: @basename@ file-tail filename: @filename@ basename: @basename@ comment comment: {standard_bottom_comment} """.format( **result.subs ).strip(), result.out, ) def test_since(self): """Test user-provided 'since' version handling https://gitlab.gnome.org/GNOME/glib/-/merge_requests/1492""" h_contents = """ typedef enum { /*< since=1.0 >*/ QMI_WMS_MESSAGE_PROTOCOL_CDMA = 0, } QmiWmsMessageProtocol; """ result = self.runMkenumsWithHeader(h_contents) self.assertEqual("", result.err) self.assertSingleEnum( result, "QmiWmsMessageProtocol", "qmi_wms_message_protocol", "QMI_WMS_MESSAGE_PROTOCOL", "WMS_MESSAGE_PROTOCOL", "QMI", "1.0", "enum", "Enum", "ENUM", "QMI_WMS_MESSAGE_PROTOCOL_CDMA", "cdma", "0", ) def test_enum_private_public(self): """Test private/public enums. Bug #782162.""" h_contents1 = """ typedef enum { ENUM_VALUE_PUBLIC1, /*< private >*/ ENUM_VALUE_PRIVATE, } SomeEnumA """ h_contents2 = """ typedef enum { /*< private >*/ ENUM_VALUE_PRIVATE, /*< public >*/ ENUM_VALUE_PUBLIC2, } SomeEnumB; """ result = self.runMkenumsWithHeader(h_contents1) self.maxDiff = None self.assertEqual("", result.err) self.assertSingleEnum( result, "SomeEnumA", "some_enum_a", "SOME_ENUM_A", "ENUM_A", "SOME", "", "enum", "Enum", "ENUM", "ENUM_VALUE_PUBLIC1", "public1", "0", ) result = self.runMkenumsWithHeader(h_contents2) self.assertEqual("", result.err) self.assertSingleEnum( result, "SomeEnumB", "some_enum_b", "SOME_ENUM_B", "ENUM_B", "SOME", "", "enum", "Enum", "ENUM", "ENUM_VALUE_PUBLIC2", "public2", "1", ) def test_available_in(self): """Test GLIB_AVAILABLE_ENUMERATOR_IN_2_68 handling https://gitlab.gnome.org/GNOME/glib/-/issues/2327""" h_contents = """ typedef enum { G_DBUS_SERVER_FLAGS_AUTHENTICATION_REQUIRE_SAME_USER GLIB_AVAILABLE_ENUMERATOR_IN_2_68 = (1<<2) } GDBusServerFlags; """ result = self.runMkenumsWithHeader(h_contents) self.assertEqual("", result.err) self.assertSingleEnum( result, "GDBusServerFlags", "g_dbus_server_flags", "G_DBUS_SERVER_FLAGS", "DBUS_SERVER_FLAGS", "G", "", "flags", "Flags", "FLAGS", "G_DBUS_SERVER_FLAGS_AUTHENTICATION_REQUIRE_SAME_USER", "user", "4", ) def test_deprecated_in(self): """Test GLIB_DEPRECATED_ENUMERATOR_IN_2_68 handling https://gitlab.gnome.org/GNOME/glib/-/issues/2327""" h_contents = """ typedef enum { G_DBUS_SERVER_FLAGS_AUTHENTICATION_REQUIRE_SAME_USER GLIB_DEPRECATED_ENUMERATOR_IN_2_68 = (1<<2) } GDBusServerFlags; """ result = self.runMkenumsWithHeader(h_contents) self.assertEqual("", result.err) self.assertSingleEnum( result, "GDBusServerFlags", "g_dbus_server_flags", "G_DBUS_SERVER_FLAGS", "DBUS_SERVER_FLAGS", "G", "", "flags", "Flags", "FLAGS", "G_DBUS_SERVER_FLAGS_AUTHENTICATION_REQUIRE_SAME_USER", "user", "4", ) def test_deprecated_in_for(self): """Test GLIB_DEPRECATED_ENUMERATOR_IN_2_68_FOR() handling https://gitlab.gnome.org/GNOME/glib/-/issues/2327""" h_contents = """ typedef enum { G_DBUS_SERVER_FLAGS_AUTHENTICATION_REQUIRE_SAME_USER GLIB_DEPRECATED_ENUMERATOR_IN_2_68_FOR(G_DBUS_SERVER_FLAGS_AUTHENTICATION_REQUIRE_SAME_USER2) = (1<<2) } GDBusServerFlags; """ result = self.runMkenumsWithHeader(h_contents) self.assertEqual("", result.err) self.assertSingleEnum( result, "GDBusServerFlags", "g_dbus_server_flags", "G_DBUS_SERVER_FLAGS", "DBUS_SERVER_FLAGS", "G", "", "flags", "Flags", "FLAGS", "G_DBUS_SERVER_FLAGS_AUTHENTICATION_REQUIRE_SAME_USER", "user", "4", ) def test_enum_symbolic_expression(self): """Test use of symbol in value expression.""" h_contents = """ typedef enum { /*< private >*/ ENUM_VALUE_PRIVATE = 5, /*< public >*/ ENUM_VALUE_PUBLIC = ENUM_VALUE_PRIVATE + 2, } TestSymbolicEnum; """ result = self.runMkenumsWithHeader(h_contents) self.assertEqual("", result.err) self.assertSingleEnum( result, "TestSymbolicEnum", "test_symbolic_enum", "TEST_SYMBOLIC_ENUM", "SYMBOLIC_ENUM", "TEST", "", "enum", "Enum", "ENUM", "ENUM_VALUE_PUBLIC", "public", "7", ) def test_comma_in_enum_value(self): """Test use of comma in enum value.""" h_contents = """ typedef enum { ENUM_VALUE_WITH_COMMA = ',', } TestCommaEnum; """ result = self.runMkenumsWithHeader(h_contents) self.assertEqual("", result.err) self.assertSingleEnum( result, "TestCommaEnum", "test_comma_enum", "TEST_COMMA_ENUM", "COMMA_ENUM", "TEST", "", "enum", "Enum", "ENUM", "ENUM_VALUE_WITH_COMMA", "comma", 44, ) class TestRspMkenums(TestMkenums): """Run all tests again in @rspfile mode""" rspfile = True if __name__ == "__main__": unittest.main(testRunner=taptestrunner.TAPTestRunner())