mirror of
https://github.com/github/codeql.git
synced 2025-12-21 19:26:31 +01:00
Python: Add tests
A slightly complicated test setup. I wanted to both make sure I captured the semantics of Python and also the fact that the kinds of global flow we expect to see are indeed present. The code is executable, and prints out both when the execution reaches certain files, and also what values are assigned to the various attributes that are referenced throughout the program. These values are validated in the test as well. My original version used introspection to avoid referencing attributes directly (thus enabling better error diagnostics), but unfortunately that made it so that the model couldn't follow what was going on. The current setup is a bit clunky (and Python's scoping rules makes it especially so -- cf. the explicit calls to `globals` and `locals`), but I think it does the job okay.
This commit is contained in:
6
python/ql/test/experimental/import-resolution/bar.py
Normal file
6
python/ql/test/experimental/import-resolution/bar.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from trace import *
|
||||
enter(__file__)
|
||||
|
||||
bar_attr = "bar_attr"
|
||||
|
||||
exit(__file__)
|
||||
14
python/ql/test/experimental/import-resolution/foo.py
Normal file
14
python/ql/test/experimental/import-resolution/foo.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from trace import *
|
||||
enter(__file__)
|
||||
|
||||
# A simple attribute. Used in main.py
|
||||
foo_attr = "foo_attr"
|
||||
|
||||
# A private attribute. Accessible from main.py despite this.
|
||||
__private_foo_attr = "__private_foo_attr"
|
||||
|
||||
# A reexport of bar under a new name. Used in main.py
|
||||
import bar as bar_reexported #$ imports=bar as=bar_reexported
|
||||
check("bar_reexported.bar_attr", bar_reexported.bar_attr, "bar_attr", globals()) #$ prints=bar_attr
|
||||
|
||||
exit(__file__)
|
||||
41
python/ql/test/experimental/import-resolution/importflow.ql
Normal file
41
python/ql/test/experimental/import-resolution/importflow.ql
Normal file
@@ -0,0 +1,41 @@
|
||||
import python
|
||||
import semmle.python.dataflow.new.DataFlow
|
||||
import semmle.python.ApiGraphs
|
||||
import TestUtilities.InlineExpectationsTest
|
||||
|
||||
private class SourceString extends DataFlow::Node {
|
||||
string contents;
|
||||
|
||||
SourceString() {
|
||||
this.asExpr().(StrConst).getText() = contents and
|
||||
this.asExpr().getParent() instanceof Assign
|
||||
}
|
||||
|
||||
string getContents() { result = contents }
|
||||
}
|
||||
|
||||
private class ImportConfiguration extends DataFlow::Configuration {
|
||||
ImportConfiguration() { this = "ImportConfiguration" }
|
||||
|
||||
override predicate isSource(DataFlow::Node source) { source instanceof SourceString }
|
||||
|
||||
override predicate isSink(DataFlow::Node sink) {
|
||||
sink = API::moduleImport("trace").getMember("check").getACall().getArg(1)
|
||||
}
|
||||
}
|
||||
|
||||
class ResolutionTest extends InlineExpectationsTest {
|
||||
ResolutionTest() { this = "ResolutionTest" }
|
||||
|
||||
override string getARelevantTag() { result = "import" }
|
||||
|
||||
override predicate hasActualResult(Location location, string element, string tag, string value) {
|
||||
exists(DataFlow::PathNode source, DataFlow::PathNode sink, ImportConfiguration config |
|
||||
config.hasFlowPath(source, sink) and
|
||||
tag = "prints" and
|
||||
location = sink.getNode().getLocation() and
|
||||
value = source.getNode().(SourceString).getContents() and
|
||||
element = sink.getNode().toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
49
python/ql/test/experimental/import-resolution/imports.ql
Normal file
49
python/ql/test/experimental/import-resolution/imports.ql
Normal file
@@ -0,0 +1,49 @@
|
||||
import python
|
||||
import TestUtilities.InlineExpectationsTest
|
||||
import semmle.python.dataflow.new.DataFlow
|
||||
import semmle.python.dataflow.new.internal.ImportResolution
|
||||
|
||||
private class ImmediateModuleRef extends DataFlow::Node {
|
||||
Module mod;
|
||||
string alias;
|
||||
|
||||
ImmediateModuleRef() {
|
||||
this = ImportResolution::getImmediateModuleReference(mod) and
|
||||
not mod.getName() in ["__future__", "trace"] and
|
||||
this.asExpr() = any(Alias a | alias = a.getAsname().(Name).getId()).getAsname()
|
||||
}
|
||||
|
||||
Module getModule() { result = mod }
|
||||
|
||||
string getAsname() { result = alias }
|
||||
}
|
||||
|
||||
class ImportTest extends InlineExpectationsTest {
|
||||
ImportTest() { this = "ImportTest" }
|
||||
|
||||
override string getARelevantTag() { result = "imports" }
|
||||
|
||||
override predicate hasActualResult(Location location, string element, string tag, string value) {
|
||||
exists(ImmediateModuleRef ref |
|
||||
tag = "imports" and
|
||||
location = ref.getLocation() and
|
||||
value = ref.getModule().getName() and
|
||||
element = ref.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class AliasTest extends InlineExpectationsTest {
|
||||
AliasTest() { this = "AliasTest" }
|
||||
|
||||
override string getARelevantTag() { result = "as" }
|
||||
|
||||
override predicate hasActualResult(Location location, string element, string tag, string value) {
|
||||
exists(ImmediateModuleRef ref |
|
||||
tag = "as" and
|
||||
location = ref.getLocation() and
|
||||
value = ref.getAsname() and
|
||||
element = ref.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
65
python/ql/test/experimental/import-resolution/main.py
Normal file
65
python/ql/test/experimental/import-resolution/main.py
Normal file
@@ -0,0 +1,65 @@
|
||||
#! /usr/bin/env python3
|
||||
|
||||
from __future__ import print_function
|
||||
import sys
|
||||
from trace import *
|
||||
enter(__file__)
|
||||
|
||||
# A simple import. Binds foo to the foo module
|
||||
import foo #$ imports=foo as=foo
|
||||
check("foo.foo_attr", foo.foo_attr, "foo_attr", globals()) #$ prints=foo_attr
|
||||
|
||||
# Private attributes are still accessible.
|
||||
check("foo.__private_foo_attr", foo.__private_foo_attr, "__private_foo_attr", globals()) #$ prints=__private_foo_attr
|
||||
|
||||
# An aliased import, binding foo to foo_alias
|
||||
import foo as foo_alias #$ imports=foo as=foo_alias
|
||||
check("foo_alias.foo_attr", foo_alias.foo_attr, "foo_attr", globals()) #$ prints=foo_attr
|
||||
|
||||
# A reference to a reexported module
|
||||
check("foo.bar_reexported.bar_attr", foo.bar_reexported.bar_attr, "bar_attr", globals()) #$ prints=bar_attr
|
||||
|
||||
# A simple "import from" statement.
|
||||
from bar import bar_attr
|
||||
check("bar_attr", bar_attr, "bar_attr", globals()) #$ prints=bar_attr
|
||||
|
||||
# Importing an attribute from a subpackage of a package.
|
||||
from package.subpackage import subpackage_attr
|
||||
check("subpackage_attr", subpackage_attr, "subpackage_attr", globals()) #$ prints=subpackage_attr
|
||||
|
||||
# Importing a package attribute under an alias.
|
||||
from package import package_attr as package_attr_alias
|
||||
check("package_attr_alias", package_attr_alias, "package_attr", globals()) #$ prints=package_attr
|
||||
|
||||
# Importing a subpackage under an alias.
|
||||
from package import subpackage as aliased_subpackage #$ imports=package.subpackage.__init__ as=aliased_subpackage
|
||||
check("aliased_subpackage.subpackage_attr", aliased_subpackage.subpackage_attr, "subpackage_attr", globals()) #$ prints=subpackage_attr
|
||||
|
||||
def local_import():
|
||||
# Same as above, but in a local scope.
|
||||
import package.subpackage as local_subpackage #$ imports=package.subpackage.__init__ as=local_subpackage
|
||||
check("local_subpackage.subpackage_attr", local_subpackage.subpackage_attr, "subpackage_attr", locals()) #$ prints=subpackage_attr
|
||||
|
||||
local_import()
|
||||
|
||||
# Importing a subpacking using `import` and binding it to a name.
|
||||
import package.subpackage as aliased_subpackage #$ imports=package.subpackage.__init__ as=aliased_subpackage
|
||||
check("aliased_subpackage.subpackage_attr", aliased_subpackage.subpackage_attr, "subpackage_attr", globals()) #$ prints=subpackage_attr
|
||||
|
||||
# Importing without binding instead binds the top level name.
|
||||
import package.subpackage #$ imports=package.__init__ as=package
|
||||
check("package.package_attr", package.package_attr, "package_attr", globals()) #$ prints=package_attr
|
||||
|
||||
if sys.version_info[0] >= 3:
|
||||
# Importing from a namespace module.
|
||||
from namespace_package.namespace_module import namespace_module_attr
|
||||
check("namespace_module_attr", namespace_module_attr, "namespace_module_attr", globals()) #$ prints=namespace_module_attr
|
||||
|
||||
exit(__file__)
|
||||
|
||||
print()
|
||||
|
||||
if status() == 0:
|
||||
print("PASS")
|
||||
else:
|
||||
print("FAIL")
|
||||
@@ -0,0 +1,6 @@
|
||||
from trace import *
|
||||
enter(__file__)
|
||||
|
||||
namespace_module_attr = "namespace_module_attr"
|
||||
|
||||
exit(__file__)
|
||||
@@ -0,0 +1,7 @@
|
||||
from trace import *
|
||||
enter(__file__)
|
||||
|
||||
attr_used_in_subpackage = "attr_used_in_subpackage"
|
||||
package_attr = "package_attr"
|
||||
|
||||
exit(__file__)
|
||||
@@ -0,0 +1,14 @@
|
||||
from trace import *
|
||||
enter(__file__)
|
||||
|
||||
subpackage_attr = "subpackage_attr"
|
||||
|
||||
# Importing an attribute from the parent package.
|
||||
from .. import attr_used_in_subpackage as imported_attr
|
||||
check("imported_attr", imported_attr, "attr_used_in_subpackage", globals()) #$ prints=attr_used_in_subpackage
|
||||
|
||||
# Importing an irrelevant attribute from a sibling module binds the name to the module.
|
||||
from .submodule import irrelevant_attr
|
||||
check("submodule.submodule_attr", submodule.submodule_attr, "submodule_attr", globals()) #$ prints=submodule_attr
|
||||
|
||||
exit(__file__)
|
||||
@@ -0,0 +1,7 @@
|
||||
from trace import *
|
||||
enter(__file__)
|
||||
|
||||
submodule_attr = "submodule_attr"
|
||||
irrelevant_attr = "irrelevant_attr"
|
||||
|
||||
exit(__file__)
|
||||
49
python/ql/test/experimental/import-resolution/trace.py
Normal file
49
python/ql/test/experimental/import-resolution/trace.py
Normal file
@@ -0,0 +1,49 @@
|
||||
from __future__ import print_function
|
||||
|
||||
_indent_level = 0
|
||||
|
||||
_print = print
|
||||
|
||||
def print(*args, **kwargs):
|
||||
_print(" " * _indent_level, end="")
|
||||
_print(*args, **kwargs)
|
||||
|
||||
def enter(file_name):
|
||||
global _indent_level
|
||||
print("Entering {}".format(file_name))
|
||||
_indent_level += 1
|
||||
|
||||
def exit(file_name):
|
||||
global _indent_level
|
||||
_indent_level -= 1
|
||||
print("Leaving {}".format(file_name))
|
||||
|
||||
_status = 0
|
||||
|
||||
def status():
|
||||
return _status
|
||||
|
||||
def check(attr_path, actual_value, expected_value, bindings):
|
||||
parts = attr_path.split(".")
|
||||
base, parts = parts[0], parts[1:]
|
||||
if base not in bindings:
|
||||
print("Error: {} not in bindings".format(base))
|
||||
_status = 1
|
||||
return
|
||||
val = bindings[base]
|
||||
for part in parts:
|
||||
if not hasattr(val, part):
|
||||
print("Error: Unknown attribute {}".format(part))
|
||||
_status = 1
|
||||
return
|
||||
val = getattr(val, part)
|
||||
if val != actual_value:
|
||||
print("Error: Value at path {} and actual value are out of sync! {} != {}".format(attr_path, val, actual_value))
|
||||
_status = 1
|
||||
if val != expected_value:
|
||||
print("Error: Expected {} to be {}, got {}".format(attr_path, expected_value, val))
|
||||
_status = 1
|
||||
return
|
||||
print("OK: {} = {}".format(attr_path, val))
|
||||
|
||||
__all__ = ["enter", "exit", "check", "status"]
|
||||
Reference in New Issue
Block a user