From be328b1daa56d0c4d15c7a385d6da0c580ed9f93 Mon Sep 17 00:00:00 2001 From: Christopher MacGown Date: Thu, 19 Oct 2017 17:35:33 -0700 Subject: [PATCH] Implement local testing (#13) --- Makefile | 22 ++++++ test_liblambda.py | 194 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 216 insertions(+) create mode 100644 Makefile create mode 100644 test_liblambda.py diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d028a55 --- /dev/null +++ b/Makefile @@ -0,0 +1,22 @@ +PY2=python2.7 +PY3=python3.6 +PY3SETUP=yum install -q -y python36 +IMAGE=amazonlinux:latest +DEST=/code + +echo: + $(eval ENVFLAG := -e EXAMPLE=echo) + +ec2-region: + $(eval ENVFLAG := -e EXAMPLE=ec2-region) + +python2: + docker run -it $(ENVFLAG) -v $(shell pwd):$(DEST):ro $(IMAGE) $(PY2) $(DEST)/test_liblambda.py +python3: + docker run -it $(ENVFLAG) -v $(shell pwd):$(DEST):ro $(IMAGE) /bin/bash -c "$(PY3SETUP); $(PY3) $(DEST)/test_liblambda.py" + +test-echo: echo python2 python3 +test-ec2: ec2-region python2 python3 + + +.PHONY: echo ec2-region test-echo test-ec2 python2 python3 diff --git a/test_liblambda.py b/test_liblambda.py new file mode 100644 index 0000000..8723e14 --- /dev/null +++ b/test_liblambda.py @@ -0,0 +1,194 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017 Christopher MacGown +# +# Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +# copied, modified, or distributed except according to those terms. +# +# +# pylint: disable=missing-docstring + +import contextlib +import fcntl +import os +import sys +import re +import time +import unittest + +ERROR = None + +try: + import liblambda +except ImportError as err: + ERROR = err + liblambda = None + + +@contextlib.contextmanager +def capture_stdout(): + '''capture_stdout() + + Store the stdout file descriptor and redirect it to the write-side of + the created pipe. We put the read-side into non-blocking mode so that + we can capture the output later. + + This is only necessary because Rust writes directly to the stdout fd + and doing the typical sys.stdout song and dance in Python doesn't + capture things coming out of `liblambda`. + + This is a context-manager and it yields the read-side of the pipe, so + it needs to be read and closed (for good housekeeping). + ''' + + # Setup pipe + read, write = os.pipe() + read, write = os.fdopen(read, 'rb'), os.fdopen(write, 'wb') + + fcntl.fcntl(read, fcntl.F_SETFL, os.O_NONBLOCK) + + fd = sys.stdout.fileno() # pylint: disable=invalid-name + with os.fdopen(os.dup(fd), 'wb') as copied: + sys.stdout.flush() + os.dup2(write.fileno(), fd) # redirect STDOUT -> the write FD. + + try: + yield read + finally: + sys.stdout.flush() + os.dup2(copied.fileno(), fd) # redirect back. + write.close() + + +def now_in_millis(): + return int(round(time.time() * 1000.0)) + + +class FakeContext(object): # pylint: disable=too-few-public-methods,too-many-instance-attributes + def __init__(self, timeout=3000): + self._start = now_in_millis() + self._timeout = timeout + + self.function_name = 'fake_function' + self.function_version = '$LATEST' + self.invoked_function_arn = 'arn:aws:lambda:XX-TEST-1:999999999999:function:fake_function' # pylint: disable=line-too-long + self.memory_limit_in_mb = '128' + self.aws_request_id = '1f8958d8-b20b-4a3c-b8fb-78896d10a9e5' + self.log_group_name = '/aws/lambda/fake_function' + # Date and coordinates of the Battle of Hastings - just for test data. + self.log_stream_name = '1066/10/14/[$LATEST]00000000000000000000505443002915' # pylint: disable=line-too-long + + def get_remaining_time_in_millis(self): + return self._timeout - (now_in_millis() - self._start) + + +def consume(reader): + try: + return reader.read().decode(sys.stdout.encoding) + finally: + reader.close() + +class TestCrowbar(unittest.TestCase): + def setUp(self): + self.context = FakeContext(timeout=100) + + if not getattr(self, 'assertRegex', None): + # assertRegexpMatches is deprecated in 3.6, so make sure python2.7 + # calls the method the same thing. + setattr(self, 'assertRegex', self.assertRegexpMatches) + + def test_00_import_liblambda(self): + # This makes the import failure a little friendlier. + self.assertIsNotNone(liblambda, "Could not import liblambda: {}".format(ERROR)) + + @unittest.skipIf(liblambda is None, "Could not import liblambda") + @unittest.skipUnless(os.environ["EXAMPLE"] == "echo", "DISABLED") + def test_01_echo_short_timeout(self): + expectation = re.compile(r'hello cloudwatch logs from (?P\w+) ' + r'version (?P\${0,1}\w+),' + r'.(?P