diff --git a/.gitignore b/.gitignore index d4f917d..19c6cad 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ target Cargo.lock *.swp +test/*.so +test/*.zip diff --git a/test/Makefile b/test/Makefile new file mode 100644 index 0000000..e02a42d --- /dev/null +++ b/test/Makefile @@ -0,0 +1,5 @@ +include Makefile.local +include Makefile.docker + +list: + @$(MAKE) -pRrq -f $(MAKEFILE_LIST) : 2>/dev/null | awk -v RS= -F: '/^# File/,/^# Finished Make/ { if ($$1 !~ "^[#.]") { print $$1 }}' | sort diff --git a/test/Makefile.docker b/test/Makefile.docker new file mode 100644 index 0000000..93988e6 --- /dev/null +++ b/test/Makefile.docker @@ -0,0 +1,28 @@ +PY2=python2.7 +PY3=python3.6 +PY3SETUP=yum install -q -y python36 +IMAGE=amazonlinux:latest +DEST := /code +VOLUME := -v $(shell pwd):$(DEST):ro +DOCKER := docker run -it +BUILDER := ilianaw/crowbar-builder + +%.zip: + docker run --rm -a stderr -a stdout -v $(shell pwd)/../:$(DEST):ro --entrypoint "/bin/bash" $(BUILDER) -c "cd examples/$*; build.sh" > $@ + +build-ec2-regions: ec2-regions.zip + @$(eval ENVFLAG := -e EXAMPLE=ec2_regions) + unzip $< +build-echo: echo.zip + @$(eval ENVFLAG := -e EXAMPLE=echo) + unzip $< + +test-docker: + -$(DOCKER) $(ENVFLAG) $(VOLUME) $(IMAGE) $(PY2) $(DEST)/test_liblambda.py + -$(DOCKER) $(ENVFLAG) $(VOLUME) $(IMAGE) /bin/bash -c "$(PY3SETUP); $(PY3) $(DEST)/test_liblambda.py" + -rm liblambda.so + +test-all-docker: + @for target in $(EXAMPLES); do $(MAKE) build-$$target test-docker; done + +.PHONY: build-echo build-ec2-regions test-docker test-all-docker diff --git a/test/Makefile.local b/test/Makefile.local new file mode 100644 index 0000000..cd75e65 --- /dev/null +++ b/test/Makefile.local @@ -0,0 +1,42 @@ +LIBLAMBDA := liblambda.so +EXPECTED := so +EXAMPLES := echo ec2-regions + +ifeq ($(shell uname -s),Darwin) + SUFFIX := dylib +else + SUFFIX := EXPECTED +endif + +%.$(EXPECTED): + @cd ../examples/$(subst .$(EXPECTED),,$@) && cargo build --release + @cp ../examples/$(subst .$(EXPECTED),,$@)/target/release/liblambda.$(SUFFIX) $@ + +echo: echo.$(EXPECTED) + @echo "Testing the echo example:" + @echo "===============================================================================" + @echo + -@unlink $(LIBLAMBDA) 2> /dev/null || true + @$(eval ENVFLAG := EXAMPLE=echo) + @ln -s echo.$(EXPECTED) $(LIBLAMBDA) + +ec2-regions: ec2-regions.$(EXPECTED) + @echo "Testing the ec2-region example:" + @echo "===============================================================================" + @echo + -@unlink $(LIBLAMBDA) 2> /dev/null || true + @$(eval ENVFLAG := EXAMPLE=ec2_regions) + @ln -s ec2-regions.$(EXPECTED) $(LIBLAMBDA) + +test-local: + @$(ENVFLAG) python2 test_liblambda.py + @$(ENVFLAG) python3 test_liblambda.py + -@unlink $(LIBLAMBDA) + -@echo + -@echo + -@echo + +test-all-local: + @for target in $(EXAMPLES); do $(MAKE) $$target test-local; done + +.PHONY: echo ec2-regions test-local test-all-local diff --git a/test/README.md b/test/README.md new file mode 100644 index 0000000..3ce215d --- /dev/null +++ b/test/README.md @@ -0,0 +1,22 @@ +# crowbar tests + +In order to test the ec2-regions example, you'll need to export your AWS credentials as environment +variables or create a credentials file at `~/.aws/credentials` formatted like so: + + + [default] + aws_access_key_id= + aws_secret_access_key= + + +The easiest way to test is to use the `test-all-local` or `test-all-docker` Makefile targets. However, +you can use any of the below to test one or more of the examples locally or built with the builder. + + + make echo test-local # Locally build and test the 'echo' example. + make ec2-regions test-local # Locally build and test the 'ec2-regions' example. + make test-all-local # Locally build and test both examples. + + make build-echo test-docker # Build and test the 'echo' example against amazonlinux:latest + make build-ec2-regions test-docker # Build and test the 'ec2-regions' example against amazonlinux:latest + make test-all-docker # Build and test both examples against amazonlinux:latest diff --git a/test/test_liblambda.py b/test/test_liblambda.py new file mode 100644 index 0000000..5d3c95a --- /dev/null +++ b/test/test_liblambda.py @@ -0,0 +1,198 @@ +# -*- 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): # pylint: disable=no-self-use + # This makes the import failure a little friendlier. + if liblambda is None: + print("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