From 22e184e6d23a54003543225c062a61781d74a895 Mon Sep 17 00:00:00 2001
From: Michal Lesiak <michal.lesiak@eosnetwork.com>
Date: Tue, 24 Jan 2023 11:45:57 +0100
Subject: [PATCH] Added plugin test.

Added implementation details.
Add docs, tests and plugin_example to the packaging.
Added other code review fixes.
---
 docs/PLUGIN.md                    | 15 ++++++--
 packaging/generate_chocolatey.bat |  7 ++++
 packaging/generate_tarball.sh     |  7 ++++
 plugin_example/README.md          |  2 +-
 src/dune/__main__.py              |  3 ++
 src/plugin/README.md              |  1 +
 tests/test_plugin.py              | 61 +++++++++++++++++++++++++++++++
 7 files changed, 91 insertions(+), 5 deletions(-)
 create mode 100644 src/plugin/README.md
 create mode 100644 tests/test_plugin.py

diff --git a/docs/PLUGIN.md b/docs/PLUGIN.md
index a4677f8c..7eb9fd56 100644
--- a/docs/PLUGIN.md
+++ b/docs/PLUGIN.md
@@ -7,12 +7,13 @@ DUNE allows users to extend its functionality through the use of plugins. DUNE p
 2. In the aforementioned subdirectory you need to create script `main.py`.
 3. `main.py` needs to define 3 functions:
    1. `add_parsing(parser)` - function that receives instance of [argparse.ArgumentParser](https://docs.python.org/3/library/argparse.html). It is used to add new DUNE command parsing arguments.
-   2. `handle_args(args)` - function that receives populated namespace returned by [ArgumentParser.parse_args](https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser.parse_args). It is used to handle new DUNE command arguments.
-   3. (optionally) `set_dune(dune)` - function that receives instance of DUNE so the user could interact with DUNE. It might be stored for later usage if needed.
+   2.  (optionally) `set_dune(dune)` - function that receives instance of DUNE so the user could interact with DUNE. It might be stored for later usage if needed.
+   3. `handle_args(args)` - function that receives populated namespace returned by [ArgumentParser.parse_args](https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser.parse_args). It is used to handle new DUNE command arguments.
+   
 
 ## Plugin examples
 You can find example plugins in [plugin_example directory](../plugin_example/).
-To test the example plugins, copy the contents of the [../plugin_example/](../plugin_example) directory into the [../src/plugin/](../src/plugin/) directory. This way, DUNE will automatically discover the new plugins.
+To test the example plugins, copy or symbolically link the contents of the [../plugin_example/](../plugin_example) directory into the [../src/plugin/](../src/plugin/) directory. This way, DUNE will automatically discover the new plugins.
 
 ### dune_hello
 The simplest plugin, which adds `--hello` to DUNE commands. When command `dune --hello` is executed then an example output is printed.
@@ -21,4 +22,10 @@ The simplest plugin, which adds `--hello` to DUNE commands. When command `dune -
 Plugin adds command `--bootstrap-account` to DUNE commands. When it is executed 3 example accounts are created: `alice`, `bob` and `cindy`.
 Additionally the contract `eosio.token` is deployed to all above accounts.
 
-In this example you can see how `set_dune` function is being used to store `dune` instance and later use it to create and prepare accounts.
\ No newline at end of file
+In this example you can see how `set_dune` function is being used to store `dune` instance and later use it to create and prepare accounts.
+
+## Implementation details
+DUNE starts with auto-discovering the plugins in the `src/plugin` subdirectories and dynamically loading each `main.py` file. The functions from each plugin are then called in the following order:
+1. `add_parsing(parser)` - this function is called first to add parsing arguments. Users can also initialize their plugin at this stage, however, it should be noted that at this point it is not known if the plugin will be used.
+2. (optionally) `set_dune(dune)` - if the user wants to interact with DUNE, they should store the DUNE object in this function.
+3. `handle_args(args)` - the user should check if their parsing arguments are being used and handle them in this function. This is the main function where the plugin does its job. The DUNE object is usually needed in this function.
diff --git a/packaging/generate_chocolatey.bat b/packaging/generate_chocolatey.bat
index da781d77..8dba411c 100644
--- a/packaging/generate_chocolatey.bat
+++ b/packaging/generate_chocolatey.bat
@@ -19,6 +19,13 @@ mkdir %target-dir%\src\dune
 copy "%script-path%"\..\src\dune\* %target-dir%\src\dune
 mkdir %target-dir%\scripts\
 copy "%script-path%"\..\scripts\* %target-dir%\scripts\
+mkdir %target-dir%\tests\
+copy "%script-path%"\..\tests\* %target-dir%\tests\
+mkdir %target-dir%\plugin_example\
+copy "%script-path%"\..\plugin_example\* %target-dir%\plugin_example\
+mkdir %target-dir%\docs\
+copy "%script-path%"\..\docs\* %target-dir%\docs\
+copy "%script-path%"\..\README* %target-dir%
 
 cd %script-path%\antelopeio-dune
 
diff --git a/packaging/generate_tarball.sh b/packaging/generate_tarball.sh
index 0e31de22..d5bddf76 100755
--- a/packaging/generate_tarball.sh
+++ b/packaging/generate_tarball.sh
@@ -5,6 +5,9 @@ DUNE_PREFIX="$PREFIX"/"$SUBPREFIX"
 mkdir -p "$DUNE_PREFIX"/scripts
 mkdir -p "$DUNE_PREFIX"/src
 mkdir -p "$DUNE_PREFIX"/licenses
+mkdir -p "$DUNE_PREFIX"/tests
+mkdir -p "$DUNE_PREFIX"/plugin_example
+mkdir -p "$DUNE_PREFIX"/docs
 
 #echo ""$PREFIX" ** "$SUBPREFIX" ** "$DUNE_PREFIX""
 
@@ -15,6 +18,10 @@ cp -R "$BUILD_DIR"/bootstrap* "$DUNE_PREFIX"
 cp -R "$BUILD_DIR"/src/* "$DUNE_PREFIX"/src
 cp -R "$BUILD_DIR"/scripts/* "$DUNE_PREFIX"/scripts
 cp -R "$BUILD_DIR"/LICENSE* "$DUNE_PREFIX"/licenses
+cp -R "$BUILD_DIR"/tests/* "$DUNE_PREFIX"/tests
+cp -R "$BUILD_DIR"/plugin_example/* "$DUNE_PREFIX"/plugin_example
+cp -R "$BUILD_DIR"/docs/* "$DUNE_PREFIX"/docs
+cp -R "$BUILD_DIR"/README* "$DUNE_PREFIX"
 # Add symbolic link
 mkdir ./usr/bin
 ln -s /"$DUNE_PREFIX/dune" ./usr/bin/antelopeio-dune
diff --git a/plugin_example/README.md b/plugin_example/README.md
index b3ea4bbb..0b46631d 100644
--- a/plugin_example/README.md
+++ b/plugin_example/README.md
@@ -1,5 +1,5 @@
 ## DUNE plugins
 
-This directory contains DUNE example plugins. To test the example plugins, copy the contents of the [../plugin_example/](../plugin_example) directory into the [../src/plugin/](../src/plugin/) directory. This way, DUNE will automatically discover the new plugins.
+This directory contains DUNE example plugins. To test the example plugins, copy or symbolically link the contents of the [../plugin_example/](../plugin_example) directory into the [../src/plugin/](../src/plugin/) directory. This way, DUNE will automatically discover the new plugins.
 
 For more information please check [plugin documentation](../docs/PLUGIN.md)
\ No newline at end of file
diff --git a/src/dune/__main__.py b/src/dune/__main__.py
index 498cc302..4d391d40 100644
--- a/src/dune/__main__.py
+++ b/src/dune/__main__.py
@@ -37,6 +37,9 @@ def load_all_modules_from_dir(plugin_dir):
 
     for subdir in os.listdir(plugin_dir):
         subdir_path = os.path.join(plugin_dir, subdir)
+        if not os.path.isdir(subdir_path):
+            continue
+        
         main_py = os.path.join(subdir_path, 'main.py')
         if not os.path.exists(main_py):
             print(f'main.py not found in {subdir_path}')
diff --git a/src/plugin/README.md b/src/plugin/README.md
new file mode 100644
index 00000000..a3910e4d
--- /dev/null
+++ b/src/plugin/README.md
@@ -0,0 +1 @@
+[README for plugins can be found in docs/PLUGIN.md](../../docs/PLUGIN.md)
\ No newline at end of file
diff --git a/tests/test_plugin.py b/tests/test_plugin.py
new file mode 100644
index 00000000..ff143027
--- /dev/null
+++ b/tests/test_plugin.py
@@ -0,0 +1,61 @@
+#!/usr/bin/env python3
+
+"""Test DUNE Plugin
+
+This script tests that after copying plugin to src/plugin DUNE detects it and run properly.
+"""
+
+import os
+import shutil
+import subprocess
+
+from common import DUNE_EXE
+
+current_script_path = os.path.abspath(__file__)
+current_script_dir = os.path.dirname(current_script_path)
+
+src_dir = current_script_dir + '/../plugin_example/dune_hello'
+dst_dir = current_script_dir + '/../src/plugin/dune_hello'
+
+def prepare_plugin():
+    remove_plugin()
+    shutil.copytree(src_dir, dst_dir)
+
+def remove_plugin():
+    shutil.rmtree(dst_dir, ignore_errors=True)
+
+def test_plugin_help():
+    prepare_plugin()
+
+    expect_list = \
+        [
+            b'--hello',
+        ]
+
+    # Call DUNE.
+    completed_process = subprocess.run([DUNE_EXE,"--help"], check=True, stdout=subprocess.PIPE)
+
+    # Test for expected values in the captured output.
+    for expect in expect_list:
+        assert expect in completed_process.stdout
+
+    remove_plugin()
+
+
+def test_plugin_execution():
+    prepare_plugin()
+
+    expect_list = \
+        [
+            b'Hello from DUNE',
+        ]
+
+    # Call DUNE.
+    completed_process = subprocess.run([DUNE_EXE,"--hello"], check=True, stdout=subprocess.PIPE)
+
+    # Test for expected values in the captured output.
+    for expect in expect_list:
+        assert expect in completed_process.stdout
+
+    remove_plugin()
+    
\ No newline at end of file