Better error message for multiple defined symbols.
Test: nose2 Test: m ndk Bug: http://b/116629622 Change-Id: I30719eaf29d63d8c6595bbab4e5214a1ce6189ca
This commit is contained in:
@@ -20,6 +20,7 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
ALL_ARCHITECTURES = (
|
ALL_ARCHITECTURES = (
|
||||||
@@ -107,22 +108,33 @@ def version_is_private(version):
|
|||||||
return version.endswith('_PRIVATE') or version.endswith('_PLATFORM')
|
return version.endswith('_PRIVATE') or version.endswith('_PLATFORM')
|
||||||
|
|
||||||
|
|
||||||
def should_omit_version(name, tags, arch, api, vndk):
|
def should_omit_version(version, arch, api, vndk):
|
||||||
"""Returns True if the version section should be ommitted.
|
"""Returns True if the version section should be ommitted.
|
||||||
|
|
||||||
We want to omit any sections that do not have any symbols we'll have in the
|
We want to omit any sections that do not have any symbols we'll have in the
|
||||||
stub library. Sections that contain entirely future symbols or only symbols
|
stub library. Sections that contain entirely future symbols or only symbols
|
||||||
for certain architectures.
|
for certain architectures.
|
||||||
"""
|
"""
|
||||||
if version_is_private(name):
|
if version_is_private(version.name):
|
||||||
return True
|
return True
|
||||||
if 'platform-only' in tags:
|
if 'platform-only' in version.tags:
|
||||||
return True
|
return True
|
||||||
if 'vndk' in tags and not vndk:
|
if 'vndk' in version.tags and not vndk:
|
||||||
return True
|
return True
|
||||||
if not symbol_in_arch(tags, arch):
|
if not symbol_in_arch(version.tags, arch):
|
||||||
return True
|
return True
|
||||||
if not symbol_in_api(tags, arch, api):
|
if not symbol_in_api(version.tags, arch, api):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def should_omit_symbol(symbol, arch, api, vndk):
|
||||||
|
"""Returns True if the symbol should be omitted."""
|
||||||
|
if not vndk and 'vndk' in symbol.tags:
|
||||||
|
return True
|
||||||
|
if not symbol_in_arch(symbol.tags, arch):
|
||||||
|
return True
|
||||||
|
if not symbol_in_api(symbol.tags, arch, api):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -189,6 +201,15 @@ class ParseError(RuntimeError):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MultiplyDefinedSymbolError(RuntimeError):
|
||||||
|
"""A symbol name was multiply defined."""
|
||||||
|
def __init__(self, multiply_defined_symbols):
|
||||||
|
super(MultiplyDefinedSymbolError, self).__init__(
|
||||||
|
'Version script contains multiple definitions for: {}'.format(
|
||||||
|
', '.join(multiply_defined_symbols)))
|
||||||
|
self.multiply_defined_symbols = multiply_defined_symbols
|
||||||
|
|
||||||
|
|
||||||
class Version(object):
|
class Version(object):
|
||||||
"""A version block of a symbol file."""
|
"""A version block of a symbol file."""
|
||||||
def __init__(self, name, base, tags, symbols):
|
def __init__(self, name, base, tags, symbols):
|
||||||
@@ -221,9 +242,12 @@ class Symbol(object):
|
|||||||
|
|
||||||
class SymbolFileParser(object):
|
class SymbolFileParser(object):
|
||||||
"""Parses NDK symbol files."""
|
"""Parses NDK symbol files."""
|
||||||
def __init__(self, input_file, api_map):
|
def __init__(self, input_file, api_map, arch, api, vndk):
|
||||||
self.input_file = input_file
|
self.input_file = input_file
|
||||||
self.api_map = api_map
|
self.api_map = api_map
|
||||||
|
self.arch = arch
|
||||||
|
self.api = api
|
||||||
|
self.vndk = vndk
|
||||||
self.current_line = None
|
self.current_line = None
|
||||||
|
|
||||||
def parse(self):
|
def parse(self):
|
||||||
@@ -235,8 +259,36 @@ class SymbolFileParser(object):
|
|||||||
else:
|
else:
|
||||||
raise ParseError(
|
raise ParseError(
|
||||||
'Unexpected contents at top level: ' + self.current_line)
|
'Unexpected contents at top level: ' + self.current_line)
|
||||||
|
|
||||||
|
self.check_no_duplicate_symbols(versions)
|
||||||
return versions
|
return versions
|
||||||
|
|
||||||
|
def check_no_duplicate_symbols(self, versions):
|
||||||
|
"""Raises errors for multiply defined symbols.
|
||||||
|
|
||||||
|
This situation is the normal case when symbol versioning is actually
|
||||||
|
used, but this script doesn't currently handle that. The error message
|
||||||
|
will be a not necessarily obvious "error: redefition of 'foo'" from
|
||||||
|
stub.c, so it's better for us to catch this situation and raise a
|
||||||
|
better error.
|
||||||
|
"""
|
||||||
|
symbol_names = set()
|
||||||
|
multiply_defined_symbols = set()
|
||||||
|
for version in versions:
|
||||||
|
if should_omit_version(version, self.arch, self.api, self.vndk):
|
||||||
|
continue
|
||||||
|
|
||||||
|
for symbol in version.symbols:
|
||||||
|
if should_omit_symbol(symbol, self.arch, self.api, self.vndk):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if symbol.name in symbol_names:
|
||||||
|
multiply_defined_symbols.add(symbol.name)
|
||||||
|
symbol_names.add(symbol.name)
|
||||||
|
if multiply_defined_symbols:
|
||||||
|
raise MultiplyDefinedSymbolError(
|
||||||
|
sorted(list(multiply_defined_symbols)))
|
||||||
|
|
||||||
def parse_version(self):
|
def parse_version(self):
|
||||||
"""Parses a single version section and returns a Version object."""
|
"""Parses a single version section and returns a Version object."""
|
||||||
name = self.current_line.split('{')[0].strip()
|
name = self.current_line.split('{')[0].strip()
|
||||||
@@ -325,20 +377,14 @@ class Generator(object):
|
|||||||
|
|
||||||
def write_version(self, version):
|
def write_version(self, version):
|
||||||
"""Writes a single version block's data to the output files."""
|
"""Writes a single version block's data to the output files."""
|
||||||
name = version.name
|
if should_omit_version(version, self.arch, self.api, self.vndk):
|
||||||
tags = version.tags
|
|
||||||
if should_omit_version(name, tags, self.arch, self.api, self.vndk):
|
|
||||||
return
|
return
|
||||||
|
|
||||||
section_versioned = symbol_versioned_in_api(tags, self.api)
|
section_versioned = symbol_versioned_in_api(version.tags, self.api)
|
||||||
version_empty = True
|
version_empty = True
|
||||||
pruned_symbols = []
|
pruned_symbols = []
|
||||||
for symbol in version.symbols:
|
for symbol in version.symbols:
|
||||||
if not self.vndk and 'vndk' in symbol.tags:
|
if should_omit_symbol(symbol, self.arch, self.api, self.vndk):
|
||||||
continue
|
|
||||||
if not symbol_in_arch(symbol.tags, self.arch):
|
|
||||||
continue
|
|
||||||
if not symbol_in_api(symbol.tags, self.arch, self.api):
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if symbol_versioned_in_api(symbol.tags, self.api):
|
if symbol_versioned_in_api(symbol.tags, self.api):
|
||||||
@@ -433,7 +479,11 @@ def main():
|
|||||||
logging.basicConfig(level=verbose_map[verbosity])
|
logging.basicConfig(level=verbose_map[verbosity])
|
||||||
|
|
||||||
with open(args.symbol_file) as symbol_file:
|
with open(args.symbol_file) as symbol_file:
|
||||||
versions = SymbolFileParser(symbol_file, api_map).parse()
|
try:
|
||||||
|
versions = SymbolFileParser(symbol_file, api_map, args.arch, api,
|
||||||
|
args.vndk).parse()
|
||||||
|
except MultiplyDefinedSymbolError as ex:
|
||||||
|
sys.exit('{}: error: {}'.format(args.symbol_file, ex))
|
||||||
|
|
||||||
with open(args.stub_src, 'w') as src_file:
|
with open(args.stub_src, 'w') as src_file:
|
||||||
with open(args.version_script, 'w') as version_file:
|
with open(args.version_script, 'w') as version_file:
|
||||||
|
@@ -163,39 +163,94 @@ class SymbolPresenceTest(unittest.TestCase):
|
|||||||
|
|
||||||
class OmitVersionTest(unittest.TestCase):
|
class OmitVersionTest(unittest.TestCase):
|
||||||
def test_omit_private(self):
|
def test_omit_private(self):
|
||||||
self.assertFalse(gsl.should_omit_version('foo', [], 'arm', 9, False))
|
|
||||||
|
|
||||||
self.assertTrue(gsl.should_omit_version(
|
|
||||||
'foo_PRIVATE', [], 'arm', 9, False))
|
|
||||||
self.assertTrue(gsl.should_omit_version(
|
|
||||||
'foo_PLATFORM', [], 'arm', 9, False))
|
|
||||||
|
|
||||||
self.assertTrue(gsl.should_omit_version(
|
|
||||||
'foo', ['platform-only'], 'arm', 9, False))
|
|
||||||
|
|
||||||
def test_omit_vndk(self):
|
|
||||||
self.assertTrue(gsl.should_omit_version(
|
|
||||||
'foo', ['vndk'], 'arm', 9, False))
|
|
||||||
|
|
||||||
self.assertFalse(gsl.should_omit_version('foo', [], 'arm', 9, True))
|
|
||||||
self.assertFalse(gsl.should_omit_version(
|
|
||||||
'foo', ['vndk'], 'arm', 9, True))
|
|
||||||
|
|
||||||
def test_omit_arch(self):
|
|
||||||
self.assertFalse(gsl.should_omit_version('foo', [], 'arm', 9, False))
|
|
||||||
self.assertFalse(gsl.should_omit_version(
|
|
||||||
'foo', ['arm'], 'arm', 9, False))
|
|
||||||
|
|
||||||
self.assertTrue(gsl.should_omit_version(
|
|
||||||
'foo', ['x86'], 'arm', 9, False))
|
|
||||||
|
|
||||||
def test_omit_api(self):
|
|
||||||
self.assertFalse(gsl.should_omit_version('foo', [], 'arm', 9, False))
|
|
||||||
self.assertFalse(
|
self.assertFalse(
|
||||||
gsl.should_omit_version('foo', ['introduced=9'], 'arm', 9, False))
|
gsl.should_omit_version(
|
||||||
|
gsl.Version('foo', None, [], []), 'arm', 9, False))
|
||||||
|
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
gsl.should_omit_version('foo', ['introduced=14'], 'arm', 9, False))
|
gsl.should_omit_version(
|
||||||
|
gsl.Version('foo_PRIVATE', None, [], []), 'arm', 9, False))
|
||||||
|
self.assertTrue(
|
||||||
|
gsl.should_omit_version(
|
||||||
|
gsl.Version('foo_PLATFORM', None, [], []), 'arm', 9, False))
|
||||||
|
|
||||||
|
self.assertTrue(
|
||||||
|
gsl.should_omit_version(
|
||||||
|
gsl.Version('foo', None, ['platform-only'], []), 'arm', 9,
|
||||||
|
False))
|
||||||
|
|
||||||
|
def test_omit_vndk(self):
|
||||||
|
self.assertTrue(
|
||||||
|
gsl.should_omit_version(
|
||||||
|
gsl.Version('foo', None, ['vndk'], []), 'arm', 9, False))
|
||||||
|
|
||||||
|
self.assertFalse(
|
||||||
|
gsl.should_omit_version(
|
||||||
|
gsl.Version('foo', None, [], []), 'arm', 9, True))
|
||||||
|
self.assertFalse(
|
||||||
|
gsl.should_omit_version(
|
||||||
|
gsl.Version('foo', None, ['vndk'], []), 'arm', 9, True))
|
||||||
|
|
||||||
|
def test_omit_arch(self):
|
||||||
|
self.assertFalse(
|
||||||
|
gsl.should_omit_version(
|
||||||
|
gsl.Version('foo', None, [], []), 'arm', 9, False))
|
||||||
|
self.assertFalse(
|
||||||
|
gsl.should_omit_version(
|
||||||
|
gsl.Version('foo', None, ['arm'], []), 'arm', 9, False))
|
||||||
|
|
||||||
|
self.assertTrue(
|
||||||
|
gsl.should_omit_version(
|
||||||
|
gsl.Version('foo', None, ['x86'], []), 'arm', 9, False))
|
||||||
|
|
||||||
|
def test_omit_api(self):
|
||||||
|
self.assertFalse(
|
||||||
|
gsl.should_omit_version(
|
||||||
|
gsl.Version('foo', None, [], []), 'arm', 9, False))
|
||||||
|
self.assertFalse(
|
||||||
|
gsl.should_omit_version(
|
||||||
|
gsl.Version('foo', None, ['introduced=9'], []), 'arm', 9,
|
||||||
|
False))
|
||||||
|
|
||||||
|
self.assertTrue(
|
||||||
|
gsl.should_omit_version(
|
||||||
|
gsl.Version('foo', None, ['introduced=14'], []), 'arm', 9,
|
||||||
|
False))
|
||||||
|
|
||||||
|
|
||||||
|
class OmitSymbolTest(unittest.TestCase):
|
||||||
|
def test_omit_vndk(self):
|
||||||
|
self.assertTrue(
|
||||||
|
gsl.should_omit_symbol(
|
||||||
|
gsl.Symbol('foo', ['vndk']), 'arm', 9, False))
|
||||||
|
|
||||||
|
self.assertFalse(
|
||||||
|
gsl.should_omit_symbol(gsl.Symbol('foo', []), 'arm', 9, True))
|
||||||
|
self.assertFalse(
|
||||||
|
gsl.should_omit_symbol(
|
||||||
|
gsl.Symbol('foo', ['vndk']), 'arm', 9, True))
|
||||||
|
|
||||||
|
def test_omit_arch(self):
|
||||||
|
self.assertFalse(
|
||||||
|
gsl.should_omit_symbol(gsl.Symbol('foo', []), 'arm', 9, False))
|
||||||
|
self.assertFalse(
|
||||||
|
gsl.should_omit_symbol(
|
||||||
|
gsl.Symbol('foo', ['arm']), 'arm', 9, False))
|
||||||
|
|
||||||
|
self.assertTrue(
|
||||||
|
gsl.should_omit_symbol(
|
||||||
|
gsl.Symbol('foo', ['x86']), 'arm', 9, False))
|
||||||
|
|
||||||
|
def test_omit_api(self):
|
||||||
|
self.assertFalse(
|
||||||
|
gsl.should_omit_symbol(gsl.Symbol('foo', []), 'arm', 9, False))
|
||||||
|
self.assertFalse(
|
||||||
|
gsl.should_omit_symbol(
|
||||||
|
gsl.Symbol('foo', ['introduced=9']), 'arm', 9, False))
|
||||||
|
|
||||||
|
self.assertTrue(
|
||||||
|
gsl.should_omit_symbol(
|
||||||
|
gsl.Symbol('foo', ['introduced=14']), 'arm', 9, False))
|
||||||
|
|
||||||
|
|
||||||
class SymbolFileParseTest(unittest.TestCase):
|
class SymbolFileParseTest(unittest.TestCase):
|
||||||
@@ -207,7 +262,7 @@ class SymbolFileParseTest(unittest.TestCase):
|
|||||||
# baz
|
# baz
|
||||||
qux
|
qux
|
||||||
"""))
|
"""))
|
||||||
parser = gsl.SymbolFileParser(input_file, {})
|
parser = gsl.SymbolFileParser(input_file, {}, 'arm', 16, False)
|
||||||
self.assertIsNone(parser.current_line)
|
self.assertIsNone(parser.current_line)
|
||||||
|
|
||||||
self.assertEqual('foo', parser.next_line().strip())
|
self.assertEqual('foo', parser.next_line().strip())
|
||||||
@@ -232,7 +287,7 @@ class SymbolFileParseTest(unittest.TestCase):
|
|||||||
VERSION_2 {
|
VERSION_2 {
|
||||||
} VERSION_1; # asdf
|
} VERSION_1; # asdf
|
||||||
"""))
|
"""))
|
||||||
parser = gsl.SymbolFileParser(input_file, {})
|
parser = gsl.SymbolFileParser(input_file, {}, 'arm', 16, False)
|
||||||
|
|
||||||
parser.next_line()
|
parser.next_line()
|
||||||
version = parser.parse_version()
|
version = parser.parse_version()
|
||||||
@@ -256,7 +311,7 @@ class SymbolFileParseTest(unittest.TestCase):
|
|||||||
input_file = io.StringIO(textwrap.dedent("""\
|
input_file = io.StringIO(textwrap.dedent("""\
|
||||||
VERSION_1 {
|
VERSION_1 {
|
||||||
"""))
|
"""))
|
||||||
parser = gsl.SymbolFileParser(input_file, {})
|
parser = gsl.SymbolFileParser(input_file, {}, 'arm', 16, False)
|
||||||
parser.next_line()
|
parser.next_line()
|
||||||
with self.assertRaises(gsl.ParseError):
|
with self.assertRaises(gsl.ParseError):
|
||||||
parser.parse_version()
|
parser.parse_version()
|
||||||
@@ -267,7 +322,7 @@ class SymbolFileParseTest(unittest.TestCase):
|
|||||||
foo:
|
foo:
|
||||||
}
|
}
|
||||||
"""))
|
"""))
|
||||||
parser = gsl.SymbolFileParser(input_file, {})
|
parser = gsl.SymbolFileParser(input_file, {}, 'arm', 16, False)
|
||||||
parser.next_line()
|
parser.next_line()
|
||||||
with self.assertRaises(gsl.ParseError):
|
with self.assertRaises(gsl.ParseError):
|
||||||
parser.parse_version()
|
parser.parse_version()
|
||||||
@@ -277,7 +332,7 @@ class SymbolFileParseTest(unittest.TestCase):
|
|||||||
foo;
|
foo;
|
||||||
bar; # baz qux
|
bar; # baz qux
|
||||||
"""))
|
"""))
|
||||||
parser = gsl.SymbolFileParser(input_file, {})
|
parser = gsl.SymbolFileParser(input_file, {}, 'arm', 16, False)
|
||||||
|
|
||||||
parser.next_line()
|
parser.next_line()
|
||||||
symbol = parser.parse_symbol()
|
symbol = parser.parse_symbol()
|
||||||
@@ -295,7 +350,7 @@ class SymbolFileParseTest(unittest.TestCase):
|
|||||||
*;
|
*;
|
||||||
};
|
};
|
||||||
"""))
|
"""))
|
||||||
parser = gsl.SymbolFileParser(input_file, {})
|
parser = gsl.SymbolFileParser(input_file, {}, 'arm', 16, False)
|
||||||
parser.next_line()
|
parser.next_line()
|
||||||
with self.assertRaises(gsl.ParseError):
|
with self.assertRaises(gsl.ParseError):
|
||||||
parser.parse_version()
|
parser.parse_version()
|
||||||
@@ -307,7 +362,7 @@ class SymbolFileParseTest(unittest.TestCase):
|
|||||||
*;
|
*;
|
||||||
};
|
};
|
||||||
"""))
|
"""))
|
||||||
parser = gsl.SymbolFileParser(input_file, {})
|
parser = gsl.SymbolFileParser(input_file, {}, 'arm', 16, False)
|
||||||
parser.next_line()
|
parser.next_line()
|
||||||
version = parser.parse_version()
|
version = parser.parse_version()
|
||||||
self.assertEqual([], version.symbols)
|
self.assertEqual([], version.symbols)
|
||||||
@@ -318,7 +373,7 @@ class SymbolFileParseTest(unittest.TestCase):
|
|||||||
foo
|
foo
|
||||||
};
|
};
|
||||||
"""))
|
"""))
|
||||||
parser = gsl.SymbolFileParser(input_file, {})
|
parser = gsl.SymbolFileParser(input_file, {}, 'arm', 16, False)
|
||||||
parser.next_line()
|
parser.next_line()
|
||||||
with self.assertRaises(gsl.ParseError):
|
with self.assertRaises(gsl.ParseError):
|
||||||
parser.parse_version()
|
parser.parse_version()
|
||||||
@@ -326,7 +381,7 @@ class SymbolFileParseTest(unittest.TestCase):
|
|||||||
def test_parse_fails_invalid_input(self):
|
def test_parse_fails_invalid_input(self):
|
||||||
with self.assertRaises(gsl.ParseError):
|
with self.assertRaises(gsl.ParseError):
|
||||||
input_file = io.StringIO('foo')
|
input_file = io.StringIO('foo')
|
||||||
parser = gsl.SymbolFileParser(input_file, {})
|
parser = gsl.SymbolFileParser(input_file, {}, 'arm', 16, False)
|
||||||
parser.parse()
|
parser.parse()
|
||||||
|
|
||||||
def test_parse(self):
|
def test_parse(self):
|
||||||
@@ -347,7 +402,7 @@ class SymbolFileParseTest(unittest.TestCase):
|
|||||||
qwerty;
|
qwerty;
|
||||||
} VERSION_1;
|
} VERSION_1;
|
||||||
"""))
|
"""))
|
||||||
parser = gsl.SymbolFileParser(input_file, {})
|
parser = gsl.SymbolFileParser(input_file, {}, 'arm', 16, False)
|
||||||
versions = parser.parse()
|
versions = parser.parse()
|
||||||
|
|
||||||
expected = [
|
expected = [
|
||||||
@@ -505,7 +560,7 @@ class IntegrationTest(unittest.TestCase):
|
|||||||
wobble;
|
wobble;
|
||||||
} VERSION_4;
|
} VERSION_4;
|
||||||
"""))
|
"""))
|
||||||
parser = gsl.SymbolFileParser(input_file, api_map)
|
parser = gsl.SymbolFileParser(input_file, api_map, 'arm', 9, False)
|
||||||
versions = parser.parse()
|
versions = parser.parse()
|
||||||
|
|
||||||
src_file = io.StringIO()
|
src_file = io.StringIO()
|
||||||
@@ -555,7 +610,7 @@ class IntegrationTest(unittest.TestCase):
|
|||||||
*;
|
*;
|
||||||
};
|
};
|
||||||
"""))
|
"""))
|
||||||
parser = gsl.SymbolFileParser(input_file, api_map)
|
parser = gsl.SymbolFileParser(input_file, api_map, 'arm', 9001, False)
|
||||||
versions = parser.parse()
|
versions = parser.parse()
|
||||||
|
|
||||||
src_file = io.StringIO()
|
src_file = io.StringIO()
|
||||||
@@ -578,6 +633,38 @@ class IntegrationTest(unittest.TestCase):
|
|||||||
""")
|
""")
|
||||||
self.assertEqual(expected_version, version_file.getvalue())
|
self.assertEqual(expected_version, version_file.getvalue())
|
||||||
|
|
||||||
|
def test_multiple_definition(self):
|
||||||
|
input_file = io.StringIO(textwrap.dedent("""\
|
||||||
|
VERSION_1 {
|
||||||
|
global:
|
||||||
|
foo;
|
||||||
|
foo;
|
||||||
|
bar;
|
||||||
|
baz;
|
||||||
|
qux; # arm
|
||||||
|
local:
|
||||||
|
*;
|
||||||
|
};
|
||||||
|
|
||||||
|
VERSION_2 {
|
||||||
|
global:
|
||||||
|
bar;
|
||||||
|
qux; # arm64
|
||||||
|
} VERSION_1;
|
||||||
|
|
||||||
|
VERSION_PRIVATE {
|
||||||
|
global:
|
||||||
|
baz;
|
||||||
|
} VERSION_2;
|
||||||
|
|
||||||
|
"""))
|
||||||
|
parser = gsl.SymbolFileParser(input_file, {}, 'arm', 16, False)
|
||||||
|
|
||||||
|
with self.assertRaises(gsl.MultiplyDefinedSymbolError) as cm:
|
||||||
|
parser.parse()
|
||||||
|
self.assertEquals(['bar', 'foo'],
|
||||||
|
cm.exception.multiply_defined_symbols)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
suite = unittest.TestLoader().loadTestsFromName(__name__)
|
suite = unittest.TestLoader().loadTestsFromName(__name__)
|
||||||
|
Reference in New Issue
Block a user