diff options
| author | J08nY | 2020-06-13 19:34:26 +0200 |
|---|---|---|
| committer | J08nY | 2020-06-13 19:36:00 +0200 |
| commit | 7e51c6546d369ec46a6ae8978147e79f2f0195a3 (patch) | |
| tree | c34c825cbb7dfbeb167a75961e8d9c4f5ddba4d8 | |
| parent | bec7fbe0996481bbebab80b18d781f408d96aae6 (diff) | |
| download | pyecsca-7e51c6546d369ec46a6ae8978147e79f2f0195a3.tar.gz pyecsca-7e51c6546d369ec46a6ae8978147e79f2f0195a3.tar.zst pyecsca-7e51c6546d369ec46a6ae8978147e79f2f0195a3.zip | |
Add a way for actions to have a result.
| -rw-r--r-- | pyecsca/ec/context.py | 27 | ||||
| -rw-r--r-- | pyecsca/ec/formula.py | 11 | ||||
| -rw-r--r-- | pyecsca/ec/key_agreement.py | 8 | ||||
| -rw-r--r-- | pyecsca/ec/key_generation.py | 8 | ||||
| -rw-r--r-- | pyecsca/ec/mod.py | 8 | ||||
| -rw-r--r-- | pyecsca/ec/mult.py | 52 | ||||
| -rw-r--r-- | pyecsca/ec/op.py | 4 | ||||
| -rw-r--r-- | pyecsca/ec/point.py | 14 | ||||
| -rw-r--r-- | pyecsca/sca/target/ectester.py | 62 | ||||
| -rw-r--r-- | test/ec/test_context.py | 6 |
10 files changed, 115 insertions, 85 deletions
diff --git a/pyecsca/ec/context.py b/pyecsca/ec/context.py index d01f40e..6a267e6 100644 --- a/pyecsca/ec/context.py +++ b/pyecsca/ec/context.py @@ -26,6 +26,33 @@ class Action(object): @public +class ResultAction(Action): + """An action that has a result.""" + _result: Any = None + _has_result: bool = False + + @property + def result(self) -> Any: + if not self._has_result: + raise AttributeError("No result set") + return self._result + + def exit(self, result: Any): + if not self.inside: + raise RuntimeError("Result set outside of action scope") + if self._has_result: + return + self._has_result = True + self._result = result + return result + + def __exit__(self, exc_type, exc_val, exc_tb): + if not self._has_result and exc_type is None and exc_val is None and exc_tb is None: + raise RuntimeError("Result unset on action exit") + super().__exit__(exc_type, exc_val, exc_tb) + + +@public class Tree(OrderedDict): def get_by_key(self, path: List) -> Any: diff --git a/pyecsca/ec/formula.py b/pyecsca/ec/formula.py index f380ebc..c10c79e 100644 --- a/pyecsca/ec/formula.py +++ b/pyecsca/ec/formula.py @@ -1,12 +1,12 @@ from abc import ABC, abstractmethod -from ast import parse, Expression, Mult, Add, Sub, Pow, Div +from ast import parse, Expression from itertools import product from typing import List, Set, Any, ClassVar, MutableMapping, Tuple, Union from pkg_resources import resource_stream from public import public -from .context import Action +from .context import ResultAction from .mod import Mod from .op import CodeOp, OpType @@ -35,7 +35,7 @@ class OpResult(object): @public -class FormulaAction(Action): +class FormulaAction(ResultAction): """An execution of a formula, on some input points and parameters, with some outputs.""" formula: "Formula" inputs: MutableMapping[str, Mod] @@ -124,7 +124,7 @@ class Formula(ABC): action.add_result(point, **full_resulting) result.append(point) - return tuple(result) + return action.exit(tuple(result)) def __str__(self): return f"{self.shortname}[{self.name}]" @@ -189,7 +189,8 @@ class Formula(ABC): @property def num_addsubs(self) -> int: """Number of additions and subtractions.""" - return len(list(filter(lambda op: op.operator == OpType.Add or op.operator == OpType.Sub, self.code))) + return len(list( + filter(lambda op: op.operator == OpType.Add or op.operator == OpType.Sub, self.code))) class EFDFormula(Formula): diff --git a/pyecsca/ec/key_agreement.py b/pyecsca/ec/key_agreement.py index 6d6728f..7d139fe 100644 --- a/pyecsca/ec/key_agreement.py +++ b/pyecsca/ec/key_agreement.py @@ -3,7 +3,7 @@ from typing import Optional, Any from public import public -from .context import Action +from .context import ResultAction from .mod import Mod from .mult import ScalarMultiplier from .params import DomainParameters @@ -11,7 +11,7 @@ from .point import Point @public -class ECDHAction(Action): +class ECDHAction(ResultAction): """An ECDH key exchange.""" params: DomainParameters hash_algo: Optional[Any] @@ -64,7 +64,7 @@ class KeyAgreement(object): :return: The shared secret. """ - with ECDHAction(self.params, self.hash_algo, self.privkey, self.pubkey): + with ECDHAction(self.params, self.hash_algo, self.privkey, self.pubkey) as action: affine_point = self.perform_raw() x = int(affine_point.x) p = self.params.curve.prime @@ -72,7 +72,7 @@ class KeyAgreement(object): result = x.to_bytes(n, byteorder="big") if self.hash_algo is not None: result = self.hash_algo(result).digest() - return result + return action.exit(result) @public diff --git a/pyecsca/ec/key_generation.py b/pyecsca/ec/key_generation.py index 0813dbd..6476cac 100644 --- a/pyecsca/ec/key_generation.py +++ b/pyecsca/ec/key_generation.py @@ -2,7 +2,7 @@ from typing import Tuple from public import public -from .context import Action +from .context import ResultAction from .mod import Mod from .mult import ScalarMultiplier from .params import DomainParameters @@ -10,7 +10,7 @@ from .point import Point @public -class KeygenAction(Action): +class KeygenAction(ResultAction): """A key generation.""" params: DomainParameters @@ -36,9 +36,9 @@ class KeyGeneration(object): self.affine = affine def generate(self) -> Tuple[Mod, Point]: - with KeygenAction(self.params): + with KeygenAction(self.params) as action: privkey = Mod.random(self.params.order) pubkey = self.mult.multiply(privkey.x) if self.affine: pubkey = pubkey.to_affine() - return privkey, pubkey + return action.exit((privkey, pubkey)) diff --git a/pyecsca/ec/mod.py b/pyecsca/ec/mod.py index 421f521..f59fcf4 100644 --- a/pyecsca/ec/mod.py +++ b/pyecsca/ec/mod.py @@ -4,7 +4,7 @@ from functools import wraps, lru_cache from public import public -from .context import Action +from .context import ResultAction @public @@ -82,7 +82,7 @@ def check(func): @public -class RandomModAction(Action): +class RandomModAction(ResultAction): """A random sampling from Z_n.""" order: int @@ -217,8 +217,8 @@ class Mod(object): @staticmethod def random(n: int): - with RandomModAction(n): - return Mod(secrets.randbelow(n), n) + with RandomModAction(n) as action: + return action.exit(Mod(secrets.randbelow(n), n)) def __int__(self): return self.x diff --git a/pyecsca/ec/mult.py b/pyecsca/ec/mult.py index 57c034a..6dfd5cd 100644 --- a/pyecsca/ec/mult.py +++ b/pyecsca/ec/mult.py @@ -4,7 +4,7 @@ from typing import Mapping, Tuple, Optional, MutableMapping, ClassVar, Set, Type from public import public -from .context import Action +from .context import ResultAction from .formula import (Formula, AdditionFormula, DoublingFormula, DifferentialAdditionFormula, ScalingFormula, LadderFormula, NegationFormula) from .naf import naf, wnaf @@ -13,7 +13,7 @@ from .point import Point @public -class ScalarMultiplicationAction(Action): +class ScalarMultiplicationAction(ResultAction): """A scalar multiplication of a point on a curve by a scalar.""" point: Point scalar: int @@ -135,9 +135,9 @@ class LTRMultiplier(ScalarMultiplier): def multiply(self, scalar: int) -> Point: if not self._initialized: raise ValueError("ScalaMultiplier not initialized.") - with ScalarMultiplicationAction(self._point, scalar): + with ScalarMultiplicationAction(self._point, scalar) as action: if scalar == 0: - return copy(self._params.curve.neutral) + return action.exit(copy(self._params.curve.neutral)) if self.complete: q = self._point r = copy(self._params.curve.neutral) @@ -155,7 +155,7 @@ class LTRMultiplier(ScalarMultiplier): self._add(r, q) if "scl" in self.formulas: r = self._scl(r) - return r + return action.exit(r) @public @@ -177,9 +177,9 @@ class RTLMultiplier(ScalarMultiplier): def multiply(self, scalar: int) -> Point: if not self._initialized: raise ValueError("ScalaMultiplier not initialized.") - with ScalarMultiplicationAction(self._point, scalar): + with ScalarMultiplicationAction(self._point, scalar) as action: if scalar == 0: - return copy(self._params.curve.neutral) + return action.exit(copy(self._params.curve.neutral)) q = self._point r = copy(self._params.curve.neutral) while scalar > 0: @@ -192,7 +192,7 @@ class RTLMultiplier(ScalarMultiplier): scalar >>= 1 if "scl" in self.formulas: r = self._scl(r) - return r + return action.exit(r) class CoronMultiplier(ScalarMultiplier): @@ -213,9 +213,9 @@ class CoronMultiplier(ScalarMultiplier): def multiply(self, scalar: int) -> Point: if not self._initialized: raise ValueError("ScalaMultiplier not initialized.") - with ScalarMultiplicationAction(self._point, scalar): + with ScalarMultiplicationAction(self._point, scalar) as action: if scalar == 0: - return copy(self._params.curve.neutral) + return action.exit(copy(self._params.curve.neutral)) q = self._point p0 = copy(q) for i in range(scalar.bit_length() - 2, -1, -1): @@ -225,7 +225,7 @@ class CoronMultiplier(ScalarMultiplier): p0 = p1 if "scl" in self.formulas: p0 = self._scl(p0) - return p0 + return action.exit(p0) @public @@ -247,9 +247,9 @@ class LadderMultiplier(ScalarMultiplier): def multiply(self, scalar: int) -> Point: if not self._initialized: raise ValueError("ScalaMultiplier not initialized.") - with ScalarMultiplicationAction(self._point, scalar): + with ScalarMultiplicationAction(self._point, scalar) as action: if scalar == 0: - return copy(self._params.curve.neutral) + return action.exit(copy(self._params.curve.neutral)) q = self._point if self.complete: p0 = copy(self._params.curve.neutral) @@ -266,7 +266,7 @@ class LadderMultiplier(ScalarMultiplier): p1, p0 = self._ladd(q, p1, p0) if "scl" in self.formulas: p0 = self._scl(p0) - return p0 + return action.exit(p0) @public @@ -286,9 +286,9 @@ class SimpleLadderMultiplier(ScalarMultiplier): def multiply(self, scalar: int) -> Point: if not self._initialized: raise ValueError("ScalaMultiplier not initialized.") - with ScalarMultiplicationAction(self._point, scalar): + with ScalarMultiplicationAction(self._point, scalar) as action: if scalar == 0: - return copy(self._params.curve.neutral) + return action.exit(copy(self._params.curve.neutral)) if self.complete: top = self._params.order.bit_length() - 1 else: @@ -304,7 +304,7 @@ class SimpleLadderMultiplier(ScalarMultiplier): p1 = self._dbl(p1) if "scl" in self.formulas: p0 = self._scl(p0) - return p0 + return action.exit(p0) @public @@ -324,9 +324,9 @@ class DifferentialLadderMultiplier(ScalarMultiplier): def multiply(self, scalar: int) -> Point: if not self._initialized: raise ValueError("ScalaMultiplier not initialized.") - with ScalarMultiplicationAction(self._point, scalar): + with ScalarMultiplicationAction(self._point, scalar) as action: if scalar == 0: - return copy(self._params.curve.neutral) + return action.exit(copy(self._params.curve.neutral)) if self.complete: top = self._params.order.bit_length() - 1 else: @@ -343,7 +343,7 @@ class DifferentialLadderMultiplier(ScalarMultiplier): p1 = self._dbl(p1) if "scl" in self.formulas: p0 = self._scl(p0) - return p0 + return action.exit(p0) @public @@ -366,9 +366,9 @@ class BinaryNAFMultiplier(ScalarMultiplier): def multiply(self, scalar: int) -> Point: if not self._initialized: raise ValueError("ScalaMultiplier not initialized.") - with ScalarMultiplicationAction(self._point, scalar): + with ScalarMultiplicationAction(self._point, scalar) as action: if scalar == 0: - return copy(self._params.curve.neutral) + return action.exit(copy(self._params.curve.neutral)) bnaf = naf(scalar) q = copy(self._params.curve.neutral) for val in bnaf: @@ -379,7 +379,7 @@ class BinaryNAFMultiplier(ScalarMultiplier): q = self._add(q, self._point_neg) if "scl" in self.formulas: q = self._scl(q) - return q + return action.exit(q) @public @@ -416,9 +416,9 @@ class WindowNAFMultiplier(ScalarMultiplier): def multiply(self, scalar: int) -> Point: if not self._initialized: raise ValueError("ScalaMultiplier not initialized.") - with ScalarMultiplicationAction(self._point, scalar): + with ScalarMultiplicationAction(self._point, scalar) as action: if scalar == 0: - return copy(self._params.curve.neutral) + return action.exit(copy(self._params.curve.neutral)) naf = wnaf(scalar, self.width) q = copy(self._params.curve.neutral) for val in naf: @@ -433,4 +433,4 @@ class WindowNAFMultiplier(ScalarMultiplier): q = self._add(q, neg) if "scl" in self.formulas: q = self._scl(q) - return q + return action.exit(q) diff --git a/pyecsca/ec/op.py b/pyecsca/ec/op.py index 637b627..ea0c1ad 100644 --- a/pyecsca/ec/op.py +++ b/pyecsca/ec/op.py @@ -6,7 +6,7 @@ from typing import FrozenSet, cast, Any, Optional from public import public -from .context import Action +from .context import ResultAction from .mod import Mod @@ -113,7 +113,7 @@ class CodeOp(object): @public -class OperationAction(Action): +class OperationAction(ResultAction): """An operation.""" operation: CodeOp diff --git a/pyecsca/ec/point.py b/pyecsca/ec/point.py index fe515ae..268f6f4 100644 --- a/pyecsca/ec/point.py +++ b/pyecsca/ec/point.py @@ -3,14 +3,14 @@ from typing import Mapping, Any from public import public -from .context import Action +from .context import ResultAction from .coordinates import AffineCoordinateModel, CoordinateModel from .mod import Mod, Undefined from .op import CodeOp @public -class CoordinateMappingAction(Action): +class CoordinateMappingAction(ResultAction): """A mapping of a point from one coordinate system to another one, usually one is an affine one.""" model_from: CoordinateModel model_to: CoordinateModel @@ -48,9 +48,9 @@ class Point(object): def to_affine(self) -> "Point": """Convert this point into the affine coordinate model, if possible.""" affine_model = AffineCoordinateModel(self.coordinate_model.curve_model) - with CoordinateMappingAction(self.coordinate_model, affine_model, self): + with CoordinateMappingAction(self.coordinate_model, affine_model, self) as action: if isinstance(self.coordinate_model, AffineCoordinateModel): - return copy(self) + return action.exit(copy(self)) ops = list() for s in self.coordinate_model.satisfying: try: @@ -66,12 +66,12 @@ class Point(object): locals[op.result] = op(**locals) if op.result in affine_model.variables: result[op.result] = locals[op.result] - return Point(affine_model, **result) + return action.exit(Point(affine_model, **result)) @staticmethod def from_affine(coordinate_model: CoordinateModel, affine_point: "Point") -> "Point": """Convert an affine point into a given coordinate model, if possible.""" - with CoordinateMappingAction(affine_point.coordinate_model, coordinate_model, affine_point): + with CoordinateMappingAction(affine_point.coordinate_model, coordinate_model, affine_point) as action: if not isinstance(affine_point.coordinate_model, AffineCoordinateModel): raise ValueError result = {} @@ -87,7 +87,7 @@ class Point(object): result[var] = Mod(affine_point.coords["x"] * affine_point.coords["y"], n) else: raise NotImplementedError - return Point(coordinate_model, **result) + return action.exit(Point(coordinate_model, **result)) def equals(self, other: Any) -> bool: """Test whether this point is equal to `other` irrespective of the coordinate model (in the affine sense).""" diff --git a/pyecsca/sca/target/ectester.py b/pyecsca/sca/target/ectester.py index e69695a..05028b4 100644 --- a/pyecsca/sca/target/ectester.py +++ b/pyecsca/sca/target/ectester.py @@ -17,7 +17,7 @@ from ...ec.params import DomainParameters from ...ec.point import Point -class ShiftableFlag(IntFlag): +class ShiftableFlag(IntFlag): # pragma: no cover def __lshift__(self, other): val = int(self) << other for e in self.__class__: @@ -44,14 +44,14 @@ class ShiftableFlag(IntFlag): @public -class KeypairEnum(ShiftableFlag): +class KeypairEnum(ShiftableFlag): # pragma: no cover KEYPAIR_LOCAL = 0x01 KEYPAIR_REMOTE = 0x02 KEYPAIR_BOTH = KEYPAIR_LOCAL | KEYPAIR_REMOTE @public -class InstructionEnum(IntEnum): +class InstructionEnum(IntEnum): # pragma: no cover INS_ALLOCATE = 0x5a INS_CLEAR = 0x5b INS_SET = 0x5c @@ -73,13 +73,13 @@ class InstructionEnum(IntEnum): @public -class KeyBuildEnum(IntEnum): +class KeyBuildEnum(IntEnum): # pragma: no cover BUILD_KEYPAIR = 0x01 BUILD_KEYBUILDER = 0x02 @public -class ExportEnum(IntEnum): +class ExportEnum(IntEnum): # pragma: no cover EXPORT_TRUE = 0xff EXPORT_FALSE = 0x00 @@ -89,32 +89,32 @@ class ExportEnum(IntEnum): @public -class RunModeEnum(IntEnum): +class RunModeEnum(IntEnum): # pragma: no cover MODE_NORMAL = 0xaa MODE_DRY_RUN = 0xbb @public -class KeyEnum(ShiftableFlag): +class KeyEnum(ShiftableFlag): # pragma: no cover PUBLIC = 0x01 PRIVATE = 0x02 BOTH = PRIVATE | PUBLIC @public -class AppletBaseEnum(IntEnum): +class AppletBaseEnum(IntEnum): # pragma: no cover BASE_221 = 0x0221 BASE_222 = 0x0222 @public -class KeyClassEnum(IntEnum): +class KeyClassEnum(IntEnum): # pragma: no cover ALG_EC_F2M = 4 ALG_EC_FP = 5 @public -class KeyAgreementEnum(IntEnum): +class KeyAgreementEnum(IntEnum): # pragma: no cover ALG_EC_SVDP_DH = 1 ALG_EC_SVDP_DH_KDF = 1 ALG_EC_SVDP_DHC = 2 @@ -126,7 +126,7 @@ class KeyAgreementEnum(IntEnum): @public -class SignatureEnum(IntEnum): +class SignatureEnum(IntEnum): # pragma: no cover ALG_ECDSA_SHA = 17 ALG_ECDSA_SHA_224 = 37 ALG_ECDSA_SHA_256 = 33 @@ -135,7 +135,7 @@ class SignatureEnum(IntEnum): @public -class TransformationEnum(ShiftableFlag): +class TransformationEnum(ShiftableFlag): # pragma: no cover NONE = 0x00 FIXED = 0x01 FULLRANDOM = 0x02 @@ -151,14 +151,14 @@ class TransformationEnum(ShiftableFlag): @public -class FormatEnum(IntEnum): +class FormatEnum(IntEnum): # pragma: no cover UNCOMPRESSED = 0 COMPRESSED = 1 HYBRID = 2 @public -class CurveEnum(IntEnum): +class CurveEnum(IntEnum): # pragma: no cover default = 0x00 external = 0xff secp112r1 = 0x01 @@ -177,7 +177,7 @@ class CurveEnum(IntEnum): @public -class ParameterEnum(ShiftableFlag): +class ParameterEnum(ShiftableFlag): # pragma: no cover NONE = 0x00 FP = 0x01 F2M = 0x02 @@ -195,11 +195,11 @@ class ParameterEnum(ShiftableFlag): @public -class ChunkingException(Exception): +class ChunkingException(Exception): # pragma: no cover pass -class Response(ABC): +class Response(ABC): # pragma: no cover resp: ResponseAPDU sws: List[int] params: List[bytes] @@ -245,7 +245,7 @@ class Response(ABC): @public -class AllocateKaResponse(Response): +class AllocateKaResponse(Response): # pragma: no cover """A response to the KeyAgreement allocation command.""" def __init__(self, resp: ResponseAPDU): @@ -253,7 +253,7 @@ class AllocateKaResponse(Response): @public -class AllocateSigResponse(Response): +class AllocateSigResponse(Response): # pragma: no cover """A response to the Signature allocation command.""" def __init__(self, resp: ResponseAPDU): @@ -261,7 +261,7 @@ class AllocateSigResponse(Response): @public -class AllocateResponse(Response): +class AllocateResponse(Response): # pragma: no cover """A response to the KeyPair allocation command.""" def __init__(self, resp: ResponseAPDU, keypair: KeypairEnum): @@ -269,7 +269,7 @@ class AllocateResponse(Response): @public -class ClearResponse(Response): +class ClearResponse(Response): # pragma: no cover """A response to the Clear key command.""" def __init__(self, resp: ResponseAPDU, keypair: KeypairEnum): @@ -277,7 +277,7 @@ class ClearResponse(Response): @public -class SetResponse(Response): +class SetResponse(Response): # pragma: no cover """A response to the Set command.""" def __init__(self, resp: ResponseAPDU, keypair: KeypairEnum): @@ -285,7 +285,7 @@ class SetResponse(Response): @public -class TransformResponse(Response): +class TransformResponse(Response): # pragma: no cover """A response to the Transform command.""" def __init__(self, resp: ResponseAPDU, keypair: KeypairEnum): @@ -293,7 +293,7 @@ class TransformResponse(Response): @public -class GenerateResponse(Response): +class GenerateResponse(Response): # pragma: no cover """A response to the Generate command.""" def __init__(self, resp: ResponseAPDU, keypair: KeypairEnum): @@ -301,7 +301,7 @@ class GenerateResponse(Response): @public -class ExportResponse(Response): +class ExportResponse(Response): # pragma: no cover """A response to the Export command, contains the exported parameters/values.""" keypair: KeypairEnum key: KeyEnum @@ -364,7 +364,7 @@ class ExportResponse(Response): @public -class ECDHResponse(Response): +class ECDHResponse(Response): # pragma: no cover """A response to the ECDH and ECDH_direct KeyAgreement commands.""" def __init__(self, resp: ResponseAPDU, export: bool): @@ -381,7 +381,7 @@ class ECDHResponse(Response): @public -class ECDSAResponse(Response): +class ECDSAResponse(Response): # pragma: no cover """A response to the ECDSA and ECDSA sign and ECDSA verify commands.""" def __init__(self, resp: ResponseAPDU, export: bool): @@ -398,7 +398,7 @@ class ECDSAResponse(Response): @public -class CleanupResponse(Response): +class CleanupResponse(Response): # pragma: no cover """A response to the Cleanup command.""" def __init__(self, resp: ResponseAPDU): @@ -406,14 +406,14 @@ class CleanupResponse(Response): @public -class RunModeResponse(Response): +class RunModeResponse(Response): # pragma: no cover """A response to the Set run mode command.""" def __init__(self, resp: ResponseAPDU): super().__init__(resp, 1, 0) -class InfoResponse(Response): +class InfoResponse(Response): # pragma: no cover """A response to the Info command, contains all information about the applet version/environment.""" version: str base: AppletBaseEnum @@ -458,7 +458,7 @@ class InfoResponse(Response): @public -class ECTesterTarget(PCSCTarget): +class ECTesterTarget(PCSCTarget): # pragma: no cover """ A smartcard target which communicates with the `ECTester <https://github.com/crocs-muni/ECTester>`_ applet on smartcards of the JavaCard platform using PCSC. diff --git a/test/ec/test_context.py b/test/ec/test_context.py index 20fcb7e..be84eb2 100644 --- a/test/ec/test_context.py +++ b/test/ec/test_context.py @@ -64,10 +64,12 @@ class ContextTests(TestCase): self.addCleanup(resetcontext, token) with local(DefaultContext()) as ctx: - self.mult.multiply(59) + result = self.mult.multiply(59) self.assertEqual(len(ctx.actions), 1) - self.assertIsInstance(next(iter(ctx.actions.keys())), ScalarMultiplicationAction) + action = next(iter(ctx.actions.keys())) + self.assertIsInstance(action, ScalarMultiplicationAction) self.assertEqual(len(getcontext().actions), 0) + self.assertEqual(result, action.result) def test_default_no_enter(self): with local(DefaultContext()) as default: |
