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:
Taus
2022-10-17 13:42:24 +00:00
parent 651afaf11b
commit ad13fbaeb6
12 changed files with 258 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
from trace import *
enter(__file__)
bar_attr = "bar_attr"
exit(__file__)

View 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__)

View 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()
)
}
}

View 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()
)
}
}

View 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")

View File

@@ -0,0 +1,6 @@
from trace import *
enter(__file__)
namespace_module_attr = "namespace_module_attr"
exit(__file__)

View File

@@ -0,0 +1,7 @@
from trace import *
enter(__file__)
attr_used_in_subpackage = "attr_used_in_subpackage"
package_attr = "package_attr"
exit(__file__)

View 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__)

View File

@@ -0,0 +1,7 @@
from trace import *
enter(__file__)
submodule_attr = "submodule_attr"
irrelevant_attr = "irrelevant_attr"
exit(__file__)

View 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"]