diff --git a/images/jenkins-tap.png b/images/jenkins-tap.png new file mode 100644 index 0000000..3d48d26 Binary files /dev/null and b/images/jenkins-tap.png differ diff --git a/slides.ipynb b/slides.ipynb index 9854a76..9ae8107 100644 --- a/slides.ipynb +++ b/slides.ipynb @@ -1,787 +1,946 @@ { "cells": [ { "cell_type": "code", "execution_count": 1, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Switched to branch 'master'\n", "Your branch is up to date with 'origin/master'.\n", "Already up to date.\n" ] } ], "source": [ "cd ../so-workshop\n", "git checkout master\n", "git pull" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "# So, you want to go open Science ?\n", "## Testing, testing, testing !\n", "\n", "<br/>\n", "\n", "- https://c4science.ch/source/so-workshop/\n", "- https://c4science.ch/source/so-slides/\n", "\n", "<br/><br/><br/>\n", "SCITAS training with expertise from the EPFL Library<br/>\n", "Nicolas Richart and Jean-Baptiste Aubort - SCITAS" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "# Continuous Integration (CI)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "## What is CI ?\n", "\n", "- Automation of build and tests\n", "- Run tests at each commit\n", "- You need to write tests !" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "## Why use CI ?\n", "\n", "- Detect bugs when they are introduced\n", "- Detect when fixed bugs appear again (regression)\n", "- Publish code that works" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "Ensure code has a certain quality level\n", "- Linters\n", "- Test Coverage" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "# Jenkins on c4science" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "## https://c4science.ch/jobs\n", "![](images/c4s-jenkinsjob.png)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "## https://jenkins.c4science.ch/\n", "![](images/c4s-jenkins.png)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "# Example\n", "## Jenkins base" ] }, { "cell_type": "code", "execution_count": 15, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Switched to branch 'feature/jenkins-base'\n", "Your branch is up to date with 'origin/feature/jenkins-base'.\n", "\u001b[01;34m.\u001b[00m\n", "├── \u001b[01;34mcode\u001b[00m\n", "│ ├── mylib.py\n", "│ └── \u001b[01;32mtest.py\u001b[00m\n", "├── Jenkinsfile\n", "└── README\n", "\n", "1 directory, 4 files\n" ] } ], "source": [ "git checkout feature/jenkins-base\n", "tree" ] }, { "cell_type": "code", "execution_count": 11, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "#!/usr/bin/env python3\n", "\n", "class MyLib():\n", "\n", " def __init__(self):\n", " pass\n", "\n", " def MyFunc(self):\n", " return 'test'\n", "\n", "if __name__ == '__main__':\n", " pass\n" ] } ], "source": [ "cat code/mylib.py" ] }, { "cell_type": "code", "execution_count": 10, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "#!/usr/bin/env python3\n", "import unittest, pytest\n", "import mylib\n", "\n", "class Test(unittest.TestCase):\n", "\n", " def test_return(self):\n", " m = mylib.MyLib()\n", " self.assertTrue(m.MyFunc() == 'test')\n", "\n", "if __name__ == '__main__':\n", " pytest.main(['./test.py'])\n" ] } ], "source": [ "cat code/test.py" ] }, { "cell_type": "code", "execution_count": 9, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "pipeline {\n", "\n", " agent {\n", " docker {\n", " image 'python:3.7'\n", " }\n", " }\n", "\n", " environment {\n", " HOME = \"$WORKSPACE\"\n", " }\n", "\n", " stages {\n", " stage('install dependencies') {\n", " steps {\n", " sh 'pip3 install --user pytest'\n", " }\n", " }\n", " stage('run tests') {\n", " steps {\n", " sh '$HOME/.local/bin/pytest code/test.py'\n", " }\n", " }\n", " }\n", "\n", "}\n" ] } ], "source": [ "cat Jenkinsfile" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "![](images/jenkins-base.png)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "# Example\n", "## Jenkins dockerfile" ] }, { "cell_type": "code", "execution_count": 16, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Switched to branch 'feature/jenkins-dockerfile'\n", "Your branch is up to date with 'origin/feature/jenkins-dockerfile'.\n", "\u001b[01;34m.\u001b[00m\n", "├── \u001b[01;34mcode\u001b[00m\n", "│ ├── mylib.py\n", "│ └── \u001b[01;32mtest.py\u001b[00m\n", "├── Dockerfile\n", "├── Jenkinsfile\n", "└── README\n", "\n", "1 directory, 5 files\n" ] } ], "source": [ "git checkout feature/jenkins-dockerfile\n", "tree" ] }, { "cell_type": "code", "execution_count": 19, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "FROM python:3.7\n", "RUN pip3 install --no-cache-dir pytest\n" ] } ], "source": [ "cat Dockerfile" ] }, { "cell_type": "code", "execution_count": 17, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "pipeline {\n", "\n", " agent {\n", " dockerfile true \n", " }\n", "\n", " stages {\n", " stage('run tests') {\n", " steps {\n", " sh 'pytest code/test.py'\n", " }\n", " }\n", " }\n", "\n", "}\n" ] } ], "source": [ "cat Jenkinsfile" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "## Example\n", "### Jenkins junit" ] }, { "cell_type": "code", "execution_count": 8, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Switched to branch 'feature/jenkins-junit'\n", "Your branch is up to date with 'origin/feature/jenkins-junit'.\n", "\u001b[01;34m.\u001b[00m\n", "├── \u001b[01;34mcode\u001b[00m\n", "│ ├── mylib.py\n", "│ └── \u001b[01;32mtest.py\u001b[00m\n", "├── Dockerfile\n", "├── Jenkinsfile\n", "└── README\n", "\n", "1 directory, 5 files\n" ] } ], "source": [ "git checkout feature/jenkins-junit\n", "tree" ] }, { "cell_type": "code", "execution_count": 21, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "pipeline {\n", "\n", " agent {\n", " dockerfile true \n", " }\n", "\n", " stages {\n", " stage('run tests') {\n", " steps {\n", " sh 'pytest --junitxml results.xml code/test.py'\n", " }\n", " }\n", " }\n", "\n", " post {\n", " always {\n", " junit 'results.xml'\n", " }\n", " }\n", "\n", "}\n" ] } ], "source": [ "cat Jenkinsfile" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "![](images/jenkins-junit.png)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "![](images/jenkins-junit2.png)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "# C4science\n", "## Arcanist\n", "Command line tool to interact with c4science" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "### Linters\n", "\n", "> A linter or lint refers to tools that analyze source code to flag programming errors, bugs, stylistic errors, and suspicious constructs.\n", "\n", "- https://en.wikipedia.org/wiki/Lint_(software)\n", "- https://c4science.ch/w/c4science/lint/" ] }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 23, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Switched to branch 'feature/arcanist-linter'\n", "Your branch is up to date with 'origin/feature/arcanist-linter'.\n", "\u001b[01;34m.\u001b[00m\n", "├── .arclint\n", "├── \u001b[01;34mcode\u001b[00m\n", "│ ├── mylib.py\n", "│ └── \u001b[01;32mtest.py\u001b[00m\n", "├── Dockerfile\n", "├── .gitignore\n", "├── Jenkinsfile\n", "└── README\n", "\n", "1 directory, 7 files\n" ] } ], "source": [ "git checkout feature/arcanist-linter\n", "tree -a -I \".git\"" ] }, { "cell_type": "code", "execution_count": 12, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\u001b[42m\u001b[1m CONFIGURED \u001b[m\u001b[49m \u001b[1mtext\u001b[m (Basic Text Linter)\n", "\u001b[43m\u001b[1m AVAILABLE \u001b[m\u001b[49m \u001b[1mcsharp\u001b[m (C#)\n", "\u001b[43m\u001b[1m AVAILABLE \u001b[m\u001b[49m \u001b[1mcpplint\u001b[m (C++ Google's Styleguide)\n", "\u001b[43m\u001b[1m AVAILABLE \u001b[m\u001b[49m \u001b[1mcppcheck\u001b[m (C++ linter)\n", "\u001b[43m\u001b[1m AVAILABLE \u001b[m\u001b[49m \u001b[1mlessc\u001b[m (CSS pre-processor)\n", "\u001b[43m\u001b[1m AVAILABLE \u001b[m\u001b[49m \u001b[1mcsslint\u001b[m (CSSLint)\n", "\u001b[43m\u001b[1m AVAILABLE \u001b[m\u001b[49m \u001b[1mchmod\u001b[m (Chmod)\n", "\u001b[43m\u001b[1m AVAILABLE \u001b[m\u001b[49m \u001b[1mgjslint\u001b[m (Closure Linter)\n", "\u001b[43m\u001b[1m AVAILABLE \u001b[m\u001b[49m \u001b[1mcoffeelint\u001b[m (CoffeeLint)\n", "\u001b[43m\u001b[1m AVAILABLE \u001b[m\u001b[49m \u001b[1mcomposer\u001b[m (Composer Dependency Manager)\n", "\u001b[42m\u001b[1m CONFIGURED \u001b[m\u001b[49m \u001b[1mfilename\u001b[m (Filename)\n", "\u001b[43m\u001b[1m AVAILABLE \u001b[m\u001b[49m \u001b[1mgenerated\u001b[m (Generated Code)\n", "\u001b[43m\u001b[1m AVAILABLE \u001b[m\u001b[49m \u001b[1mgolint\u001b[m (Golint)\n", "\u001b[43m\u001b[1m AVAILABLE \u001b[m\u001b[49m \u001b[1mhlint\u001b[m (Haskell Linter)\n", "\u001b[43m\u001b[1m AVAILABLE \u001b[m\u001b[49m \u001b[1mjsonlint\u001b[m (JSON Lint)\n", "\u001b[43m\u001b[1m AVAILABLE \u001b[m\u001b[49m \u001b[1mjson\u001b[m (JSON Lint)\n", "\u001b[43m\u001b[1m AVAILABLE \u001b[m\u001b[49m \u001b[1mjscs\u001b[m (JavaScript Code Style)\n", "\u001b[43m\u001b[1m AVAILABLE \u001b[m\u001b[49m \u001b[1mjshint\u001b[m (JavaScript error checking)\n", "\u001b[43m\u001b[1m AVAILABLE \u001b[m\u001b[49m \u001b[1mnolint\u001b[m (Lint Disabler)\n", "\u001b[43m\u001b[1m AVAILABLE \u001b[m\u001b[49m \u001b[1mmerge-conflict\u001b[m (Merge Conflicts)\n", "\u001b[43m\u001b[1m AVAILABLE \u001b[m\u001b[49m \u001b[1mphpcs\u001b[m (PHP_CodeSniffer)\n", "\u001b[43m\u001b[1m AVAILABLE \u001b[m\u001b[49m \u001b[1mphutil-library\u001b[m (Phutil Library Linter)\n", "\u001b[42m\u001b[1m CONFIGURED \u001b[m\u001b[49m \u001b[1mpylint\u001b[m (PyLint)\n", "\u001b[43m\u001b[1m AVAILABLE \u001b[m\u001b[49m \u001b[1mflake8\u001b[m (Python Flake8 multi-linter)\n", "\u001b[43m\u001b[1m AVAILABLE \u001b[m\u001b[49m \u001b[1mpep8\u001b[m (Python PEP 8)\n", "\u001b[43m\u001b[1m AVAILABLE \u001b[m\u001b[49m \u001b[1mpyflakes\u001b[m (Python PyFlakes)\n", "\u001b[43m\u001b[1m AVAILABLE \u001b[m\u001b[49m \u001b[1mruby\u001b[m (Ruby)\n", "\u001b[43m\u001b[1m AVAILABLE \u001b[m\u001b[49m \u001b[1mrubocop\u001b[m (Ruby static code analyzer)\n", "\u001b[43m\u001b[1m AVAILABLE \u001b[m\u001b[49m \u001b[1mscript-and-regex\u001b[m (Script and Regex)\n", "\u001b[43m\u001b[1m AVAILABLE \u001b[m\u001b[49m \u001b[1mxml\u001b[m (SimpleXML Linter)\n", "\u001b[42m\u001b[1m CONFIGURED \u001b[m\u001b[49m \u001b[1mspelling\u001b[m (Spellchecker)\n", "\u001b[43m\u001b[1m AVAILABLE \u001b[m\u001b[49m \u001b[1mxhpast\u001b[m (XHPAST Lint)\n", "\u001b[43m\u001b[1m AVAILABLE \u001b[m\u001b[49m \u001b[1mphp\u001b[m (php -l)\n", "\u001b[43m\u001b[1m AVAILABLE \u001b[m\u001b[49m \u001b[1mpuppet-lint\u001b[m (puppet-lint)\n", "(Run `arc linters --verbose` for more details.)\n" ] } ], "source": [ "arc linters" ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{\n", " \"linters\": {\n", " \"spelling-linter\": {\n", " \"type\": \"spelling\"\n", " },\n", " \"filename-linter\": {\n", " \"type\": \"filename\"\n", " },\n", " \"text-linter\": {\n", " \"type\": \"text\"\n", " },\n", " \"python-checks\": {\n", " \"type\": \"pylint\",\n", " \"include\": \"(\\\\.py$)\"\n", " }\n", " }\n", "}\n" ] } ], "source": [ "cat .arclint" ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Jenkinsfile:4:Auto-Fix: Trailing Whitespace\n", "README:3:Auto-Fix: Trailing Whitespace at EOF\n", "code/mylib.py:1:Advice: Missing Docstring\n", "code/mylib.py:3:Advice: Missing Docstring\n", "code/mylib.py:3:Advice: Old Style Class\n", "code/mylib.py:8:Advice: Invalid Name\n", "code/mylib.py:8:Advice: Missing Docstring\n", "code/mylib.py:8:Advice: No Self Use\n", "code/mylib.py:11:Advice: Invalid Name\n", "code/mylib.py:11:Advice: Missing Docstring\n", "code/mylib.py:11:Advice: No Self Use\n", "code/test.py:1:Advice: Missing Docstring\n", "code/test.py:5:Advice: Missing Docstring\n", "code/test.py:9:Advice: Missing Docstring\n", "code/test.py:12:Warning: Mixed Indentation\n", "code/test.py:12:Error: Tab Literal\n", "code/test.py:12:Advice: Missing Docstring\n", "code/test.py:12:Warning: Unused Variable\n", "code/test.py:13:Warning: Mixed Indentation\n" ] }, { "ename": "", "evalue": "2", "output_type": "error", "traceback": [] } ], "source": [ "arc lint --never-apply-patches --output summary --everything" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ - "### Unit-Tests\n" + "### Unit-Tests\n", + "Using the TAP (Test Anything Protocol) extension of Arcanist, we can report test units result to c4science" ] }, { "cell_type": "code", - "execution_count": 17, - "metadata": {}, + "execution_count": 28, + "metadata": { + "slideshow": { + "slide_type": "subslide" + } + }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Switched to branch 'feature/arcanist-unittests'\n", "Your branch is up to date with 'origin/feature/arcanist-unittests'.\n", + "Submodule path '.arcanist-extensions': checked out 'b89c749a90154676967a1601b62563fed0cbd572'\n", "\u001b[01;34m.\u001b[00m\n", "├── .arcconfig\n", "├── .arclint\n", "├── \u001b[01;34mcode\u001b[00m\n", "│ ├── mylib.py\n", "│ └── \u001b[01;32mtest.py\u001b[00m\n", "├── Dockerfile\n", "├── .gitignore\n", "├── .gitmodules\n", "├── Jenkinsfile\n", "└── README\n", "\n", "1 directory, 9 files\n" ] } ], "source": [ "git checkout feature/arcanist-unittests\n", + "git submodule update --init\n", "tree -a -I \".git|.arcanist-extensions\"" ] }, { - "cell_type": "raw", + "cell_type": "code", + "execution_count": 21, "metadata": { "slideshow": { - "slide_type": "slide" + "slide_type": "subslide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " \"load\": [\n", + " \".arcanist-extensions/tap_test_engine\"\n", + " ],\n", + "\n", + " \"unit.engine\": \"TAPTestEngine\",\n", + " \"unit.engine.tap.command\": \"code/test.py\"\n", + "}\n" + ] + } + ], + "source": [ + "cat .arcconfig" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": { + "slideshow": { + "slide_type": "subslide" } }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "ok 1 - Test.test_capitalize\n", + "ok 2 - Test.test_return\n", + "1..2\n" + ] + } + ], + "source": [ + "pytest --tap-stream ./code/test.py" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "ok 1 - Test.test_capitalize\n", + "not ok 2 - Test.test_capitalize_fail\n", + "# \n", + "# self = <test.Test testMethod=test_capitalize_fail>\n", + "# \n", + "# def test_capitalize_fail(self):\n", + "# > self.assertTrue(self.m.Capitalize('TeSt') == 'test')\n", + "# E AssertionError: False is not true\n", + "# \n", + "# code/test.py:16: AssertionError\n", + "ok 3 - Test.test_return\n", + "1..3\n", + " \u001b[42m\u001b[1m PASS \u001b[m\u001b[49m \u001b[32m <1ms\u001b[39m\u001b[33m★\u001b[39m Test.test_capitalize\n", + " \u001b[41m\u001b[1m FAIL \u001b[m\u001b[49m Test.test_capitalize_fail\n", + "#\n", + " \u001b[42m\u001b[1m PASS \u001b[m\u001b[49m \u001b[32m <1ms\u001b[39m\u001b[33m★\u001b[39m Test.test_return\n" + ] + }, + { + "ename": "", + "evalue": "2", + "output_type": "error", + "traceback": [] + } + ], + "source": [ + "arc unit --rev HEAD^" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "FROM dkdde/arcanist\n", + "RUN apk add --update bash \\\n", + " git \\\n", + " php7 \\\n", + " php7-curl \\\n", + " php7-json \\\n", + " py2-pip \\\n", + " && pip install pytest pytest-tap tap.py \\\n", + " && rm -rf /var/cache/apk/*\n", + "RUN ln -s /arc/arcanist/bin/arc /bin/arc\n", + "\n", + "ENTRYPOINT []\n" + ] + } + ], + "source": [ + "cat Dockerfile" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "pipeline {\n", + "\n", + " agent {\n", + " dockerfile true\n", + " }\n", + "\n", + " stages {\n", + " stage('run tests') {\n", + " steps {\n", + " sh 'git submodule update --init'\n", + " sh 'arc unit --rev HEAD^'\n", + " }\n", + " }\n", + " }\n", + "\n", + " post {\n", + " always {\n", + " archiveArtifacts artifacts: '*.tap'\n", + " step([$class: \"TapPublisher\", testResults: \"*.tap\"])\n", + " }\n", + " }\n", + "\n", + "}\n" + ] + } + ], + "source": [ + "cat Jenkinsfile" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, "source": [ - "### Jenkins and Arcanist" + "![](images/jenkins-tap.png)" ] } ], "metadata": { "celltoolbar": "Slideshow", "kernelspec": { "display_name": "Bash", "language": "bash", "name": "bash" }, "language_info": { "codemirror_mode": "shell", "file_extension": ".sh", "mimetype": "text/x-sh", "name": "bash" }, "livereveal": { "height": 768, "theme": "serif", "transition": "none", "width": 1024 } }, "nbformat": 4, "nbformat_minor": 2 }