diff --git a/.circleci/config.yml b/.circleci/config.yml index e2d2176..8c6c3b5 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -30,9 +30,4 @@ jobs: name: run tests command: | . venv/bin/activate - python main.py test test - - - store_artifacts: - path: test-reports - destination: test-reports - + python setup.py test diff --git a/.gitignore b/.gitignore index a6266e2..ccbc678 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,100 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# IPython Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# dotenv +.env + +# virtualenv +.venv/ +venv/ +ENV/ + +# Spyder project settings +.spyderproject + +# Rope project settings +.ropeproject + +# Other +.vscode +simdem-env +hello_world +.simdem +||||||| merged common ancestors *~ \#* .#* @@ -5,8 +102,11 @@ env.local.json !demo_scripts/simdem/variables/env.local.json !demo_scripts/test/env.local.json !/env.local.json -*.log # Python -__pycache__ -simdem-env/ \ No newline at end of file +simdem-env/ +env.local.json +!demo_scripts/simdem/variables/env.local.json +!demo_scripts/test/env.local.json +!/env.local.json +*.log \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..aaf1913 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM python:3 + +WORKDIR /usr/src/app +RUN apt-get update + +COPY requirements.txt ./ + +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +RUN pip install -v -e . + +# The default prompt is # which throws off the initialization +RUN echo 'export PS1="$ "' >> /root/.bashrc + +CMD [ "simdem" ] + diff --git a/Dockerfile_cli b/Dockerfile_cli deleted file mode 100644 index b08a1f8..0000000 --- a/Dockerfile_cli +++ /dev/null @@ -1,49 +0,0 @@ -FROM ubuntu:16.04 - -ENV HOME /home/simdem -ENV TERM xterm -WORKDIR $HOME - -RUN apt-get update - -# Not really needed, but used in the SimDem demo script -RUN apt-get install tree -y - -# Azure CLI -RUN echo "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ wheezy main" | tee /etc/apt/sources.list.d/azure-cli.list -RUN apt-get install apt-transport-https -y -RUN apt-get update -RUN apt-key adv --keyserver packages.microsoft.com --recv-keys 417A0893 -RUN apt-get install azure-cli -y --allow-unauthenticated - -# Python -RUN apt-get install python3-pip -y - -# Create SimDem User -RUN apt-get install sudo -y -RUN apt-get install whois -y -RUN useradd simdem -u 1984 -p `mkpasswd password` -RUN usermod -aG sudo simdem -RUN echo "simdem ALL=NOPASSWD: ALL" >> /etc/sudoers -RUN mkdir -p $HOME && chown -R 1984 $HOME -RUN mkdir -p $HOME/.azure && chown -R 1984 $HOME/.azure -RUN mkdir -p $HOME/.ssh && chown -R 1984 $HOME/.ssh -RUN mkdir -p $HOME/demo_scripts && chown -R 1984 $HOME/demo_scripts - -# SimDem -COPY ./env.json $/env.json -COPY ./requirements.txt requirements.txt -RUN pip3 install -r requirements.txt -RUN mkdir /usr/local/bin/simdem_cli -COPY *.py /usr/local/bin/simdem_cli/ -RUN chmod +x /usr/local/bin/simdem_cli/main.py -RUN ln -s /usr/local/bin/simdem_cli/main.py /usr/local/bin/simdem - -# Demo Scripts -COPY demo_scripts/simdem demo_scripts - -USER 1984 - -ENTRYPOINT [ "simdem" ] - - diff --git a/Dockerfile_novnc b/Dockerfile_novnc deleted file mode 100644 index 3ceff8e..0000000 --- a/Dockerfile_novnc +++ /dev/null @@ -1,65 +0,0 @@ -FROM consol/ubuntu-xfce-vnc - -# FIXME: For consistency with CLI we should use /home/simdem, currently using /headless because that's what comes with novnc container -ENV HOME /headless -WORKDIR $HOME - -### VNC config -ENV NO_VNC_PORT 8080 -ENV VNC_COL_DEPTH 24 -ENV VNC_RESOLUTION 1024x768 -ENV VNC_PW vncpassword - -USER 0 - -RUN apt-get update - -# Not really needed but handy when debugging -RUN apt-get install emacs -y - -# Not really needed, but used in the SimDem demo script -RUN apt-get install tree -y - -# Azure CLI -RUN echo "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ wheezy main" | tee /etc/apt/sources.list.d/azure-cli.list -RUN apt-get install apt-transport-https -y -RUN apt-get update -RUN apt-key adv --keyserver packages.microsoft.com --recv-keys 417A0893 -RUN apt-get install azure-cli -y --allow-unauthenticated - -# Python -RUN apt-get install python3-pip -y - -# Desktop -USER 1984 -COPY ./novnc/ /headless/ -USER 0 -RUN rm .config/bg_sakuli.png -RUN rm Desktop/chromium-browser.desktop - -# Create SimDem User -USER 0 -RUN apt-get install sudo -y -RUN apt-get install whois -y -RUN useradd simdem -u 1984 -p `mkpasswd password` -d $HOME -s /bin/bash -RUN usermod -aG sudo simdem -RUN echo "simdem ALL=NOPASSWD: ALL" >> /etc/sudoers -RUN mkdir -p $HOME && chown -R 1984 $HOME -RUN mkdir -p $HOME/.azure && chown -R 1984 $HOME/.azure -RUN mkdir -p $HOME/.ssh && chown -R 1984 $HOME/.ssh -RUN mkdir -p $HOME/demo_scripts && chown -R 1984 $HOME/demo_scripts - -# SimDem -COPY ./requirements.txt requirements.txt -RUN pip3 install -r requirements.txt -RUN mkdir /usr/local/bin/simdem_cli -COPY *.py /usr/local/bin/simdem_cli/ -RUN chmod +x /usr/local/bin/simdem_cli/main.py -RUN ln -s /usr/local/bin/simdem_cli/main.py /usr/local/bin/simdem - -# Demo Scripts -COPY demo_scripts/simdem demo_scripts - -USER 1984 - -COPY demo_scripts demo_scripts diff --git a/GETTING_STARTED.md b/GETTING_STARTED.md deleted file mode 100644 index bbfe255..0000000 --- a/GETTING_STARTED.md +++ /dev/null @@ -1,25 +0,0 @@ -Assuming you have Docker installed… - -``` -docker run -it rgardler/simdem -``` - -If you don’t have Docker then: - -``` -git clone git@github.com:rgardler/simdem.git -cd simdem -./scripts/install.sh -simdem -``` - -Should tell you everything you need to get started, including show you -how to build a hello world script. - -If you prefer to read via markdown see -https://github.com/rgardler/simdem/tree/master/demo_scripts/simdem - -For the hello world tutorial see -https://github.com/rgardler/simdem/tree/master/demo_scripts/simdem/tutorial - -Feedback / bug reports / pull requests welcome diff --git a/LICENSE b/LICENSE index f7e425e..5aea611 100644 --- a/LICENSE +++ b/LICENSE @@ -19,4 +19,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE \ No newline at end of file +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..1aba38f --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include LICENSE diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..92fc0c1 --- /dev/null +++ b/Makefile @@ -0,0 +1,5 @@ +init: + pip3 install -r requirements.txt + +test: + python3 setup.py nosetests diff --git a/README.md b/README.md index 8ca1a42..fb61e81 100644 --- a/README.md +++ b/README.md @@ -1,345 +1,76 @@ -NOTICE: SimDem is undergoing a complete rewrite. What started out as a hack to solve a problem has taken on a life of its own. -Consequently the code in SimDem is, well awful! So Tommy Falgout is rewriting it. The goal is for it to be backward compatible -with the current version, but it will be much more maintainable and thus more conducive to improvement. At the time of writing -(end of Feb 2018) it's getting close to being ready to replace the current code. We [encourage you to take a look](https://github.com/Azure/simdem/tree/simdem2) - patches welcome. - +# SimDem [![CircleCI](https://circleci.com/gh/Azure/simdem.svg?style=svg)](https://circleci.com/gh/Azure/simdem) -This project provides ways to write tutorials in markdown that then -become interactive demo's and automated tests. You can run in a number -of different modes: - - * Tutorial: Displays the descriptive text of the tutorial and pauses - at code blocks to allow user interaction. - * Simulate: Does not display the descriptive text, but pauses at each - code block. When the user hits a key the command is "typed", a - second keypress executes the command. - * Test: Runs the commands and then verifies that the output is - sufficiently similar to the expected results (recorded in the - markdown file) to be considered correct. - * Script: Creates an executable bash script from the document - * Auto: allows any of the above modes to be run but without user - interaction +SimDem provides an easy way to convert tutorials written in markdown into interactive demos and automated tests. -The application can be run in either a CLI mode, which is ideal for -console based demo's and tutorials, or it can be run using NoVNC for a -browser based desktop experience - that is, using only a browser you -can have a full desktop experience. +## Features -# Try it Out +SimDem supports the following features: +* Command execution +* Environment variable injection +* Prerequisites +* Output validation -The easiest way to try SimDem out is with a Docker container and work -through the embedded tutorial. The following command will run the -latest developer version of the code (i.e. there may be errors). +Details on the complete feature list can be found in the [feature documentation](docs/features.md). -``` -docker run -it rgardler/simdem -``` +## Getting Started -This will start SimDem in CLI mode. +### Installation -## Experimental browser mode - -In addition to the web NoVNC container noted above there is an -experimental web mode available using the `--webui true` option shown -below. Once started point your browser at port 8080 on your host. +Currently, only available for installation in development mode: ``` -docker run -it -p 8080:8080 rgardler/simdem --webui true +git clone git@github.com:Azure/simdem.git +git checkout -b simdem2 remotes/origin/simdem2 +pip3 install -r requirements.txt +pip3 install -v -e . ``` -### Presenter view - -When using the Web UI it is possible to open a view on the demo that -shows only the console. This is ideal for use when delivering -presentations. Have this view visible to the audience while using the -speaker view, with full descriptive text, on the presenters machine. - -To open this in a separate window click the -'Console View' button (or browse to http://HOST:8080/console). - -## Python - -The most flexible way to run SimDem is to use the Python code -directly. This is generally best for developers of SimDem, so we -provide minimal documentation here. - -# Writing a Script - -The above command will walk you through the SimDem documentation. Pay -particular attention to the Syntax page, but the short version is that -you are writing markdown with code blocks. For more information see -`demo_scripts/simdem/tutorial/README.md`. - -## Running in a Docker Container - -There are two containers available, the 'cli' version and the 'novnc' -version. The first is command line only, the latter provides a browser -based Linux desktop envirnment in which the CLI is availale. The NoVNC -version makes it easy to do demo's with browser based steps without -having to install any software (other than Docker) on your client. +If you are experiencing issues with installation, please see these issues: +* ["simdem: command not found" error](https://github.com/Azure/simdem/issues/99) +* [cannot import name 'main'](https://github.com/Azure/simdem/issues/111) -We provide scripts that make it easy to run the container and to load -custom scripts into it. +### Running -### CLI Container - -The CLI container can be run in four modes: - -Tutorial : in which full textual descriptions are provided -Learn : similar to Tutorial mode, but users are expected to type the commands -Demo : in which no textual descriptions are shown and commands are "typed" -Test : run the tests - -#### Tutorial Mode - -It's easier to explain through action, so just run the container and -work through the interactive tutorial that is included +After installing, a great place to start is to run SimDem on its own documentation. ``` -./scripts/run.sh cli +simdem docs/README.md ``` -If you want to start execution in a different place, or load in your -own scripts provide the path as the second parameter. For example, the -following example skips the introductory text and runs the demo script -provided in the SimDem GitHub repository. +## Documentation -``` -./scripts/run.sh cli demo_scripts/simdem -``` +You can learn how how SimDem works by [reading the docs](https://github.com/Azure/simdem/tree/simdem2/docs). -#### Learn mode +Here is a [simple hello-world example](https://github.com/Azure/simdem/blob/simdem2/docs/hello_world.md). -Learn mode is similar to tutorial mode, but the user is expected to -type the commands after being provided instructions. +### Examples -``` -./scripts/run.sh cli learn -``` - -If you want to start execution in a different place, or load in your -own scripts provide the path as the second parameter. For example, the -following example skips the introductory text and runs the demo script -provided in the SimDem GitHub repository. +If you want to see existing examples, with expected output, check out the [examples](https://github.com/Azure/simdem/tree/simdem2/examples) -``` -./scripts/run.sh cli demo_scripts/simdem learn -``` +## Syntax -#### Demo mode +Currently, SimDem supports Markdown as the source document. Details on how to compose Markdown documents can be found in the [syntax documentation](docs/syntax.md). -To run the same file as a demo (that is without explanatory text and -with simulated typing) simply add a third paramater with the value -`demo` as folows: +## Built With -``` -./scripts/run.sh cli demo_scripts/simdem demo -``` - -#### Preparation mode - -In this mode only the preparation (prerequisite) steps are -executed. This is useful for setting up the environment for a -demo. Next time the demo is run all prepration steps will be -skipped. This means that steps that take a long time can be -pre-baked. - -To use this mode: - -``` -./scripts/run.sh cli demo_scripts/simdem prep -``` - -#### Test mode - -To run the same file as a series of tests use a third parameter value -of `test` as follows: - -``` -./scripts/run.sh cli demo_scripts/simdem test -``` - -Test mode is very useful in a continuous integration environment. For -example, you can configure your scripts to always use the latest -versions of tooling they depend upon and get early warning when a -change in one of those tools breaks your -scripts. The -[Azure Container Service demos GitHub repository](http://github.com/Azure/acs-demos) shows -this technique in use. - -### NoVNC Container - -When running in NoVNC mode a lightweight Linux desktop is run inside -the container you can then access that container using a browser. To -run the container use: - -``` -./scripts/run.sh -``` - -Now connect using the URL http://HOSTNAME_OR_IP:8080/?password=vncpassword - -Open a terminal and type: - -``` -simdem --help -``` - -To load your own demo scripts into this container use: - -``` -./scripts/run.sh novnc /path/to/scripts -``` - -## Python - -You can run the Python source without a Docker container. To learn -more install Python 3 and pip, then type the following commands: - -``` -sudo pip3 install -r requirements.txt -python3 main.py --help -``` - -## Azure Cloud Shell - -The CLI version of SimDem works fine in Azure Cloud Shell, but you -need to install it manually at this time. Here's how. - - -NOTE: in the current code (at the time of writing) the HEAD version -does not work because of the need to install some dependencies that -CloudShell does not like. We are therefore going to use an earlier -version of SimDem, this means some of the more recent features will -not be available. See issues https://github.com/Azure/simdem/issues/19 -and https://github.com/Azure/simdem/issues/22. - - -``` -git clone https://github.com/Azure/simdem.git -cd simdem -git checkout tags/CloudShell - -pip install -r requirements.txt - -mkdir -p ~/bin/simdem-dev -cp -r * ~/bin/simdem-dev -chmod +x ~/bin/simdem-dev/main.py - -echo 'export PATH=$PATH:~/bin/simdem-dev' >> ~/.bashrc -ln -s ~/bin/simdem-dev/main.py ~/bin/simdem -``` - -Now that you have the code and enviornment setup you will first need -to acivite the Python Virtual Environment. - -``` -source simdem-env/bin/activate -``` - -Now you can run the Simdem CLI: - -``` -simdem --version -``` - -# Hacking Guide - -If you make changes to the code the easiest way to build and redeploy -the container is with the scripts in `scripts` directory. These -scripts pull the current version number from the config.py (see -`SIMDEM_VERSION=x.y.z`). This version number is used as the defaiult -for the scripts in this folder. - -## Writing a SimDem Script - - - -## Building SimDem in Containers - -./scripts/build.sh Builds the noVNC (browser based) version of -the container with the default tag of `rgardler/simdem_novnc:x.y.z` - -## Running - -Use `./script/run.sh ` to execture the container -using two volume containers (see below). The `FLAVOR` is either -`novnc` (for the browser based version) or `cli` fo rthe command line -version.. - -`azure_data` volume container is used to maintain details of your -Azure Subscription (including login details). - -`simdem_VERSION_scripts` volume container has the scripts to execute -in the container, that is the contents in `SCRIPTS_DIR` (or `./demo_scripts` if no value provided). - -### Running the NoVNC container - -`./scripts/run.sh novnc ` runs an instance of the noVNC -container - -Once the container is running you can connect to it at `http://HOST:8080?password=vncpassword`. Open a terminal and run: - -`simdem` to run the default script - -Use `simdem --help` for more information. - -### Running the CLI container - -`./scripts/run.sh cli ` runs an instance of the CLI -container +* [Mistletoe](https://github.com/miyuchina/mistletoe) ## Contributing -This is an open source project. Don't keep your code improvements, -features and cool ideas to yourself. Please issue pull requests -against our [GitHub repo](http://github.com/rgardler/simdem). - -Be sure to use our Git pre-commit script to test your contributions -before committing, simply run the following command: - -``` -ln -s ../../pre-commit.sh .git/hooks/pre-commit -``` - -This project welcomes contributions and suggestions. Most -contributions require you to agree to a Contributor License Agreement -(CLA) declaring that you have the right to, and actually do, grant us -the rights to use your contribution. For details, visit -https://cla.microsoft.com. - -When you submit a pull request, a CLA-bot will automatically determine -whether you need to provide a CLA and decorate the PR appropriately -(e.g., label, comment). Simply follow the instructions provided by the -bot. You will only need to do this once across all repos using our -CLA. - -This project has adopted -the -[Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). -For more information see -the -[Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or -contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with -any additional questions or comments. +We would love to have you be a part of the SimDem development team. For details, see the [development documentation](docs/development.md). -## Publishing +## History -The `latest` version is built from source on each commit. To publish a -version tagged image use `./scripts/publish.sh ` script. This -will publish both the CLI and NoVNC containers if no `FLAVOR` is -provided. +SimDem v2 is a complete rewrite of SimDem v1. The latest commit for v1 can be found at: +https://github.com/Azure/simdem/tree/cb1caf17fd684e125789c26817f43eeae0e1c523 -Don't forget to bump the version number after using this script. To do -this open simdem.py and find and edit the following line (somewhere -near the top of the file): +## License -`SIMDEM_VERSION = "0.4.1"` +This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details. -# Learn more +## Acknowledgements -If you want to learn more before running the container then why not -read the interactive tutorial as -a -[markdown page on GitHub](https://github.com/rgardler/simdem/blob/master/demo_scripts/simdem/README.md). +* [Ross Gardler](https://twitter.com/rgardler) - The original creator of SimDem +* [Mi Yu](https://github.com/miyuchina) - Author of Mistletoe who provided guidance on Markdown parsing +* [Tommy Falgout](https://lastcoolnameleft.com) - Author of SimDem v2 diff --git a/cli.py b/cli.py deleted file mode 100644 index 670a370..0000000 --- a/cli.py +++ /dev/null @@ -1,471 +0,0 @@ -# A console based UI for SimDem. - -import difflib -import os -import pexpect -import pexpect.replwrap -import random -import re -import time -import sys -import colorama -import config -colorama.init(strip=None) - -PEXPECT_PROMPT = u'[PEXPECT_PROMPT>' -PEXPECT_CONTINUATION_PROMPT = u'[PEXPECT_PROMPT+' - -class Ui(object): - _shell = None - demo = None - execution_log = "" - - def __init__(self): - pass - - def prompt(self): - """Display the prompt for the user. This is intended to indicate that - the user is expected to take an action at this point. - """ - self.display(config.console_prompt, colorama.Fore.WHITE) - - def command(self, text): - """Display a command, or a part of a command tp be executed.""" - self.display(text, colorama.Fore.WHITE + colorama.Style.BRIGHT) - - def results(self, text): - """Display the results of a command execution""" - self.display(text, colorama.Fore.GREEN + colorama.Style.BRIGHT, True) - - def heading(self, text): - """Display a heading""" - self.display(text, colorama.Fore.CYAN + colorama.Style.BRIGHT, True) - self.new_line() - - def description(self, text): - """Display some descriptive text. Usually this is text from the demo - document itself. - - """ - self.display(text, colorama.Fore.CYAN) - - def information(self, text, new_line = False): - """Display some informative text. Usually this is content generated by - SimDem. Do not print a new line unless new_line == True. - - """ - self.display(text, colorama.Fore.WHITE, new_line) - - def prep_step(self, step): - """Displays a preparation step item. - """ - self.display(step["title"], colorama.Fore.MAGENTA, True) - - def next_step(self, index, title): - """Displays a next step item with an index (the number to be entered -to select it) and a title (to be displayed). - """ - self.display(index, colorama.Fore.CYAN) - self.display(title, colorama.Fore.CYAN, True) - - def instruction(self, text): - """Display an instruction for the user. - """ - self.display(text, colorama.Fore.MAGENTA, True) - - def warning(self, text): - """Display a warning to the user. - """ - self.display(text, colorama.Fore.RED + colorama.Style.BRIGHT, True) - - def new_para(self): - """Starts a new paragraph.""" - self.new_line() - self.new_line() - - def new_line(self): - """Move to the next line""" - self.display("", colorama.Fore.WHITE, True) - - def horizontal_rule(self): - self.display("\n\n============================================\n\n", colorama.Fore.WHITE) - - def clear(self): - """Clears the screen ready for anew section of the script.""" - if self.demo.is_simulation: - self.demo.current_command = "clear" - self.simulate_command() - else: - self.run_command("clear") - - def display(self, text, color, new_line=False): - """Display some text in a given color. Do not print a new line unless - new_line is set to True. - - """ - self.execution_log += color - if self.demo.output_format == "log": - print(color, end="") - - self.execution_log += text - if self.demo.output_format == "log": - print(text, end="", flush=True) - - if new_line: - self.execution_log += colorama.Style.RESET_ALL + "\n" - if self.demo.output_format == "log": - print(colorama.Style.RESET_ALL) - else: - self.execution_log += colorama.Style.RESET_ALL - if self.demo.output_format == "log": - print(colorama.Style.RESET_ALL, end="") - - def log(self, level, text): - if config.is_debug: - print(level.upper() + " : " + text) - - def request_input(self, text): - """Displays text that is intended to propmt the user for - input and then waits for input. - - """ - print(colorama.Fore.MAGENTA + colorama.Style.BRIGHT, end="") - print(text) - print(colorama.Style.RESET_ALL, end="") - return self.input_string().lower() - - def input_interactive_variable(self, name): - """ - Gets a value from stdin for a variable. - """ - print(colorama.Fore.MAGENTA + colorama.Style.BRIGHT, end="") - print("\n\nEnter a value for ", end="") - print(colorama.Fore.YELLOW + colorama.Style.BRIGHT, end="") - print("$" + name, end="") - print(colorama.Fore.MAGENTA + colorama.Style.BRIGHT, end="") - print(": ", end="") - print(colorama.Fore.WHITE + colorama.Style.BRIGHT, end="") - value = input() - return value - - def type_command(self): - """ - Displays the command on the screen - If simulation == True then it will look like someone is typing the command - """ - - text = "" - end_of_var = 0 - current_command, undefined_var_list, defined_var_list = self.demo.get_current_command() - for idx, char in enumerate(current_command): - if char != "\n": - text += char - - if self.demo.is_simulation: - for char in text: - delay = random.uniform(0.02, config.TYPING_DELAY) - time.sleep(delay) - self.command(char) - else: - self.command(text) - - - def simulate_command(self, silent = False): - """Types the current command on the screen, executes it and outputs - the results if simulation == True then system will make the - "typing" look real and will wait for keyboard entry before - proceeding to the next command. - - If silent = True then the command and its results will not be - ouptut. - """ - - self.log("debug", "Simulating command: '" + self.demo.current_command + "'") - if not self.demo.is_learning or self.demo.current_command.strip() == "clear": - self.type_command() - _, undefined_var_list, defined_var_list = self.demo.get_current_command() - - # Get values for unknown variables - for var_name in undefined_var_list: - if (self.demo.is_testing): - var_value = "Dummy value for test" - else: - var_value = self.input_interactive_variable(var_name) - if not var_name.startswith("SIMDEM_"): - self.demo.env.set(var_name, var_value) - self.run_command(var_name + '="' + var_value + '"') - - # Log values if in debug mode - if config.is_debug: - self.information("\n") - for var_name in undefined_var_list: - self.log("debug", "$" + var_name + " = " + self.demo.env.get(var_name)) - for var_name in defined_var_list: - self.log("debug", "$" + var_name + " = " + self.demo.env.get(var_name)) - - output = self.run_command() - self.demo.last_command = self.demo.current_command - self.demo.current_command = "" - else: - done = False - while not done: - print(colorama.Fore.MAGENTA + colorama.Style.BRIGHT, end="") - print("\nType the command '", end = "") - print(colorama.Fore.WHITE + colorama.Style.BRIGHT, end="") - print(self.demo.current_command.strip(), end = "") - print(colorama.Fore.MAGENTA + colorama.Style.BRIGHT, end="") - print("'") - print("\t- type 'auto' (or 'a') to automatically type the command") - print(colorama.Fore.WHITE + colorama.Style.BRIGHT, end="") - print("\n$ ", end = "", flush = True) - typed_command = input() - if typed_command.lower() == "a" or typed_command.lower() == "auto": - self.demo.is_learning = False - output = self.simulate_command() - self.demo.is_learning = True - done = True - elif typed_command == self.demo.current_command.strip(): - self.demo.is_learning = False - output = self.simulate_command() - self.demo.is_learning = True - done = True - else: - print(colorama.Fore.RED, end="") - print("You have a typo there") - - self.log("debug", "Output: '" + output +"'") - return output - - def input_string(self): - """ Get a string from the user.""" - return input() - - def get_shell(self): - """Gets or creates the shell in which to run commands for the - supplied demo - """ - if self._shell == None: - child = pexpect.spawnu('/bin/bash', env=self.demo.env.get(), echo=False, timeout=None) - ps1 = PEXPECT_PROMPT[:5] + u'\[\]' + PEXPECT_PROMPT[5:] - ps2 = PEXPECT_CONTINUATION_PROMPT[:5] + u'\[\]' + PEXPECT_CONTINUATION_PROMPT[5:] - prompt_change = u"PS1='{0}' PS2='{1}' PROMPT_COMMAND=''".format(ps1, ps2) - self._shell = pexpect.replwrap.REPLWrapper(child, u'\$', prompt_change) - return self._shell - - def run_command(self, command=None, silent = False): - """ - Run the self.demo.curent_command unless command is passed in, in - which case run the supplied command in the current demo - environment. Return the output of the command. - - A small number of commands are intercepted and handled as - special cases, see `run_special_command` - """ - if not command: - command = self.demo.current_command - - command = command.strip() - self.new_line(); - - self.log("debug", "Execute command: '" + command + "'") - start_time = time.time() - - response = self.run_special_command(command) - if response: - pass - else: - response = self.get_shell().run_command(command) - end_time = time.time() - - if not silent: - self.results(response) - - if self.demo.is_testing: - self.information("--- %s seconds execution time ---" % (end_time - start_time), True) - - return response - - def run_special_command(self, command): - """Test to see if the command is a spcial command that needs to be - handled diferently, these include: - - `xdg-open $URL` - intercepted and converted to a curl for headless CLI - `az acs create ...` - if we have a service principle set in environment variables 'SERVICE_PRINCIPAL_ID' and 'SERVICE_PRINCIPAL_SECRET_KEY' then add them to the command (assuming that we don't have a device login active) - - Returns the response from the command if it was handled by this function, - otherwise returns False. - - """ - orig_command = command - if command.startswith("xdg-open "): - self.warning("Since you are running in headless CLI mode it is not possible to execute xdg-open commands.") - - command = "curl -I " + command[9:] + " --connect-timeout 90" - - self.warning("Converting to `" + command + "`") - self.warning("Note that this may break tests.") - - if command.startswith('az acs create '): - if "--orchestrator-type=kubernetes" in command and not "--service-principal" in command: - if os.getenv('SERVICE_PRINCIPAL_ID'): - command += " --service-principal ${SERVICE_PRINCIPAL_ID} --client-secret ${SERVICE_PRINCIPAL_SECRET_KEY}" - - if orig_command != command: - self.log("INFO", "Running special command " + orig_command + " as " + command) - response = self.get_shell().run_command(command) - return response - else: - return False - - def expand_vars(self, command): - """Expand the variables in the supplied command by replacing them - with the value they carry in the Environment. This is used by some special commands because the shell doesn't expand them (e.g. copying a $URL into a browser window using xdg-open)""" - - self.log("debug", "Expanding vars in " + command) - var_pattern = re.compile(".*?(?<=\$)\(?{?(\w*)(?=[\W|\$|\s|\\\"]?)\)?(?!\$).*") - matches = var_pattern.findall(command) - if matches: - for var in matches: - value = self.demo.env.get(var) - self.log("Debug", "Expanding variable " + var + " to value " + value) - command = command.replace("$" + var, value) - return command - - def get_help(self): - help = [] - help.append("SimDem Help") - help.append("===========") - help.append("") - help.append("Pressing any key other than those listed below will result in the script progressing") - help.append("") - help.append("b - break out of the script and accept a command from user input") - help.append("b -> CTRL-C - stop the script") - help.append("d - (redisplay the description that precedes the current command then resume from this point)") - help.append("r - repeat the previous command") - help.append("h - displays this help message") - help.append("") - return help - - def check_for_interactive_command(self): - """Wait for a key to be pressed. - - Most keys result in the script - progressing, but a few have special meaning. See the - documentation or code for a description of the special keys. - """ - if not self.demo.is_automated: - if not self.demo.is_simulation: - self.instruction("Press a command key to proceed (h for help)") - key = self.get_instruction_key() - - if key == 'h': - text = self.get_help() - for line in text: - self.information(line, True) - - self.check_for_interactive_command() - elif key == 'b': - print("shell> ", end='') - command = input() - if command != "": - self.run_command(command) - self.prompt() - self.check_for_interactive_command() - elif key == 'd': - print("") - print(colorama.Fore.CYAN) - print(self.demo.current_description); - print(colorama.Style.RESET_ALL) - self.prompt() - print(self.demo.current_command, end="", flush=True) - self.check_for_interactive_command() - elif key == 'r': - if not self.demo.last_command == "": - self.demo.current_command = self.demo.last_command - self.simulate_command() - self.prompt() - self.check_for_interactive_command() - - def get_instruction_key(self): - """Waits for a single keypress on stdin. - - This is a silly function to call if you need to do it a lot because it has - to store stdin's current setup, setup stdin for reading single keystrokes - then read the single keystroke then revert stdin back after reading the - keystroke. - - Returns the character of the key that was pressed (zero on - KeyboardInterrupt which can happen when a signal gets handled) - - This method is licensed under cc by-sa 3.0 - Thanks to mheyman http://stackoverflow.com/questions/983354/how-do-i-make-python-to-wait-for-a-pressed-key\ - """ - import termios, fcntl, sys, os - fd = sys.stdin.fileno() - # save old state - flags_save = fcntl.fcntl(fd, fcntl.F_GETFL) - attrs_save = termios.tcgetattr(fd) - # make raw - the way to do this comes from the termios(3) man page. - attrs = list(attrs_save) # copy the stored version to update - # iflag - attrs[0] &= ~(termios.IGNBRK | termios.BRKINT | termios.PARMRK - | termios.ISTRIP | termios.INLCR | termios. IGNCR - | termios.ICRNL | termios.IXON ) - # oflag - attrs[1] &= ~termios.OPOST - # cflag - attrs[2] &= ~(termios.CSIZE | termios. PARENB) - attrs[2] |= termios.CS8 - # lflag - attrs[3] &= ~(termios.ECHONL | termios.ECHO | termios.ICANON - | termios.ISIG | termios.IEXTEN) - termios.tcsetattr(fd, termios.TCSANOW, attrs) - # turn off non-blocking - fcntl.fcntl(fd, fcntl.F_SETFL, flags_save & ~os.O_NONBLOCK) - # read a single keystroke - try: - ret = sys.stdin.read(1) # returns a single character - except KeyboardInterrupt: - ret = 0 - finally: - # restore old state - termios.tcsetattr(fd, termios.TCSAFLUSH, attrs_save) - fcntl.fcntl(fd, fcntl.F_SETFL, flags_save) - return ret - - def test_results(self, results): - """Display the test results for a single test - """ - if results["passed"]: - return - else: - print("\n\n=============================\n\n") - print(colorama.Fore.RED + colorama.Style.BRIGHT) - print("FAILED") - print(colorama.Style.RESET_ALL) - print("Similarity ratio: " + str(results["similarity"])) - print("Expected Similarity: " + str(results["required_similarity"])) - print("\n\n=============================\n\n") - print("Expected results:") - print(colorama.Fore.GREEN + colorama.Style.BRIGHT) - print(results["expected_results"]) - print(colorama.Style.RESET_ALL) - print("Actual results:") - print(colorama.Fore.RED + colorama.Style.BRIGHT) - print(results["results"]) - print(colorama.Style.RESET_ALL) - - print("\n\n=============================\n\n") - print(colorama.Style.RESET_ALL) - - def get_command(self, commands): - cmd = self.request_input("What mode do you want to run in? (default 'tutorial')") - - if cmd == "": - cmd = "tutorial" - while not cmd in commands: - cmd = self.get_command(commands) - return cmd - - def set_demo(self, demo): - self.demo = demo diff --git a/config.py b/config.py deleted file mode 100644 index 38d66ec..0000000 --- a/config.py +++ /dev/null @@ -1,28 +0,0 @@ -SIMDEM_VERSION = "0.8.2-dev" -SIMDEM_TEMP_DIR = "~/.simdem/tmp" - -# When in demo mode we insert a small random delay between characters. -# TYPING DELAY is the upper bound of this delay. -TYPING_DELAY = 0.2 - -# Prompt to use in the console -console_prompt = "$ " - -# Port for web server when running with '--webui true' optios -port = 8080 - -# ------------------------------------------------------------------ # -# Danger zone -# -# Do not change anything after this notice, -# unless you know what you are doing -# ------------------------------------------------------------------ # - -# Set is_debug to True if you want to run in debug mode. This setting -# can be overriden in the command like with the `--debug true` option. -is_debug = False - -# Available modes of execution -modes = [ "tutorial", "demo", "learn", "test", "script", "prep" ] - - diff --git a/demo_scripts/README.md b/demo_scripts/README.md deleted file mode 100644 index 9e3b09a..0000000 --- a/demo_scripts/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# Welcome to SimDem - -Is it: - - * Documentation - * An interactive tutorial - * A live demo - * An automated test script - * A Shell script - -## Simdem is Documentation, Tutorials, Demo's and Tests - -It's all of them! - -# Next Steps - - 1. [Hello World Demo](simdem/demo/README.md) - 2. [SimDem Documentation](simdem/README.md) - diff --git a/demo_scripts/env.json b/demo_scripts/env.json deleted file mode 100644 index 6fcf682..0000000 --- a/demo_scripts/env.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "PARENT_TEST": "Hello from the parent" -} diff --git a/demo_scripts/simdem/README.md b/demo_scripts/simdem/README.md deleted file mode 100644 index 8bb26dc..0000000 --- a/demo_scripts/simdem/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# Welcome to SimDem - -Is it: - - * Documentation - * An interactive tutorial - * A live demo - * An automated test script - * A Shell script - -## Simdem is Documentation, Tutorials, Demo's and Tests - -It's all of them! - -Simdem allows you to wite a tutorial in markdown format and then run -the commands within it as a simulated demo, interactive tutorial or -even a test script. You can also generate executable shell scripts. - -SimDem reads a script, written in the form of a human readable -Markdown file, and executes the commands within this script on your -behalf. It will even make it look like you are really typing the -commands, which is great if you want to concentrate on explaining what -you are doing but still run the demo live. - -It's easier to describe if you see it working. In fact you are already -in a SimDem. Press a key (other than 'b', we'll look at that shortly) -to "type" a command, once the command has been "typed" hit -a key to execute the command. - -# Next Steps - -Tutorials can branch too, for example you can choose any of the -following paths next: - - 1. [Modes of operation](modes/README.md) - 2. [Hello World Demo](demo/README.md) - 3. [Build a Hello World script](tutorial/README.md) - 4. [Write SimDem documents](syntax/README.md) - 5. [Special Commands](special_commands/README.md) - 6. [Configure your scripts through variables](variables/README.md) - 7. [Write multi-part documents](multipart/README.md) - 8. [Use your documents as interactive tutorials or demos](running/README.md) - 9. [Use your documents as automated tests](test/README.md) - 10. [Build an SimDem container](building/README.md) - - - - diff --git a/demo_scripts/simdem/building/README.md b/demo_scripts/simdem/building/README.md deleted file mode 100644 index 2837b1f..0000000 --- a/demo_scripts/simdem/building/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# Building your own Demo Container - -Create a Dockerfile and add (at least) the following: - - ``` - FROM rgardler/simdem - - COPY my_script_dir demo_scripts - ``` - -# Next Steps - - 1. [SimDem Index](../README.md) diff --git a/demo_scripts/simdem/env.json b/demo_scripts/simdem/env.json deleted file mode 100644 index 7a7485b..0000000 --- a/demo_scripts/simdem/env.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "TEST": "hello-world" -} diff --git a/demo_scripts/simdem/env.local.json b/demo_scripts/simdem/env.local.json deleted file mode 100644 index e6c7c04..0000000 --- a/demo_scripts/simdem/env.local.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "LOCAL_TEST": "A warm local hello" -} diff --git a/demo_scripts/simdem/modes/README.md b/demo_scripts/simdem/modes/README.md deleted file mode 100644 index 9afa34a..0000000 --- a/demo_scripts/simdem/modes/README.md +++ /dev/null @@ -1,95 +0,0 @@ -# Modes of Operation - -SimDem demos are interactive and can be run in a number of different -modes: - - * Tutorial: Displays the descriptive text of the tutorial and pauses - at code blocks to allow user interaction. - * Learn: similar to Tutorial mode, but users are expected to type - the commands themselves. - * Simulate: Does not display the descriptive text, but pauses at each - code block. When the user hits a key the command is "typed", a - second keypress executes the command. - * Test: Runs the commands and then verifies that the output is - sufficiently similar to the expected results (recorded in the - markdown file) to be considered correct. - * Script: Creates an executable bash script from the document - * Auto: allows any of the above modes to be run but without user - interaction - -## Tutorial Mode - -Tutorial mode is ideal if you are using this as a learning or teaching -tool (see also learn mode below, which suits some learning styles -better. In this mode a description of what you are about to do is -shown on the screen, hit a key to see the command, hit another key to -execute the command. Tutorial mode is the default. - -## Learn mode - -Learn mode is similar to tutorial mode above, however, in learn mode -the user is expected to type each command themselves. Some people find -that this aids recall. - -## Demo (or Simulation) mode - -Demo mode is ideal if you are using this to teach or demonstrate how -to achive the goal. In this mode no descriptive text is shown, instead -when you press a key the next command is "typed", pressing another key -will execute the command. The idea is that you describe what is -happening as the application "types" the command for you. To run in -demo mode use the `--style simulate` command line switch with any -other mode. There is also a default demo mode configuration avilable -with the `demo` command" - -``` -simdem demo -``` - -#### Preparation mode - -In this mode only the preparation (prerequisite) steps are -executed. This is useful for setting up the environment for a -demo. Next time the demo is run all prepration steps will be -skipped. This means that steps that take a long time can be -pre-baked. - -To use this mode use the command 'mode' - -``` -simdem prep -``` - -## Test Mode - -Test mode runs the commands and then verifies that the output is -sufficiently similar to the expected results (recorded in the markdown -file) to be considered correct. To run in test mode use the `--test -yes` switch. For convenience you can use the command `test` to execute -tests with the optimal configuration for automated testing.. - -## Script mode - -Script mode does not execute any of the commands, instead is outputs -an executable bash script that can be run without SimDem. Use the -command `script` to generate the executable script. - -# Unnattended (Auto) Mode - -Each of these modes can be run in auto mode too. This means that the -program does not wait for a keypress before proceeding. This can be -useful if you want to runthe complete script unattended. To run in -automated or unnattended mode use the `--auto true` command line -switch. - -Manual mode is ideal if you would like to manually type the commands, -many people find this helps them remember. It can be useful in the -first few runs, but we still recommend using "demo" mode when doing -live demo's - it's much harder to make a mistake this way. - -# Next Steps - - 1. [Hello World Demo](../demo/README.md) - 2. [SimDem Index](../README.md) - 3. [Write SimDem documents](../syntax/README.md) - 4. [Build a SimDem container](../building/README.md) diff --git a/demo_scripts/simdem/multipart/README.md b/demo_scripts/simdem/multipart/README.md deleted file mode 100644 index 18ab03b..0000000 --- a/demo_scripts/simdem/multipart/README.md +++ /dev/null @@ -1,106 +0,0 @@ -# Multi-Part Demo's - -Most tutorials's will consist of at least three parts, preparation, -main body and cleanup. Many will have multiple staged in the main part -of the tutorial. SimDem is able to provide an interactive menu -allowing users to select which part of the tutorial to work through -next. This is achieved by providing a final section with the title `# -Next Steps`. This section should include a list in which each item -provides a link to a markdown document that contain SimDem scripts -that the user may want to work through next. - -For example, this file is one part of a multi-part document. - -``` -cat $SIMDEM_CWD/README.md -``` - -When executed using SimDem this results in the user being prompted to -select a "next step" (or hit 'q' to quit). If the user selects one of -the scripts it will be executed. - -# Prerequisites - -It is assumed that you have a basic understanding of the various -SimDem execution [modes](../modes/README.md). You should also ensure -you understand the SimDem [document syntax](../syntax). - -# Directory structure - -SimDem projects consist of a root directory and one or more -sub-directories. Project directories will contain at least a -`README.md` file that will be used by default when SimDem is run -against the project. Therefore the minimum directory structure for a -simple tutorial is: - -` -My_SimDem_Tutorial -└── README.md -` - -# Multi-part tutorials - -A more complex project will contain a number of sub-directories -containing tutorials. Tutorial sub-directories will contain at least a -`README.md` file, this is the main file for that tutorial. For example: - -` -My_Complex_SimDem_Tutorial -├── README.md -├── Tutorial_1 -│ └── README.md -├── Tutorial_2 -│ └── README.md -└── Tutorial_3 - └── README.md -` - -## Auto Table of Contents - -If the root of the demo scripts directory does not contain a -`README.md` file then SimDem will create a Table of Contents from all -sub-directories that contain a `README.md` file. This ToC will use the -first line (which should be a heading marked with '# ' at the start) -as the text for the link to the script. This ToC will be displayed as -a 'Next Steps' section, thus users will be able to step into any area -of the available demo's. - -# Other files - -Tutorials may also provide an `env.json` and/or an `env.local.json` -and/or an `env.test.json` file to define environment variables to use -when executing in demo or test mode. - -# Demo Scripts example - -The directory structure for the SimDem demo scripts is: - -``` -tree $SIMDEM_CWD/.. -``` - -Results: Expected Similarity: 0.5 - -``` -demo_scripts/ -├── env.json -├── env.local.json -├── README.md -├── simdem -│ ├── env.json -│ ├── env.local.json -│ └── README.md -└── test - ├── env.json - └── README.md -``` - -# Next Steps - - 1. [Configure your scripts through variables](../variables/README.md) - 2. [SimDem Index](../README.md) - 3. [Use your documents as interactive tutorials or demos](../running/README.md) - 4. [Use your documents as automated tests](../test/README.md) - 5. [Build a SimDem container](../building/README.md) - - diff --git a/demo_scripts/simdem/prerequisites/remote.md b/demo_scripts/simdem/prerequisites/remote.md deleted file mode 100644 index 2c3adc8..0000000 --- a/demo_scripts/simdem/prerequisites/remote.md +++ /dev/null @@ -1,4 +0,0 @@ -# A remote Prerequsite - -This files is really only for test purposes. See the main -prerequisites script for more details. diff --git a/demo_scripts/simdem/running/README.md b/demo_scripts/simdem/running/README.md deleted file mode 100644 index c430b44..0000000 --- a/demo_scripts/simdem/running/README.md +++ /dev/null @@ -1,56 +0,0 @@ -# Running SimDem - -SimDem is packaged as a container, you run it with: - -`docker run -it rgardler/simdem` - -This will run the demo script you are working through now. - -## Adding Your Own Demo Script - -You will likely want to add your own demo script. To do this you can -either build your own container or you can mount a volume which -contains your demo scripts. To mount a volume run: - -`docker run -it -v ~/my_demo_dir:/demo_scripts rgardler/simdem` - -When mounting a directory the script found in the first folder within -the mounted folder will be run. If you want to run a specific demo -within that folder add `run SCRIPT_NAME` to the command, where -SCRIPT_NAME is replaced with the name of the folder containing the -script you want to run. For example: - -`docker run -it -v ~/my_demo_dir:/demo_scripts rgardler/simdem run myscript` - -# Going Off-Script - -You can go off-script if you want to. This is where you should hit 'b' -(for break). You will now be able to type a command to be -run. However, note that at the time of writing the parser for this -command is not very smart, so some commands do not work. In addition -you can't run fully interactive commands this way (so no editors for -example). Go ahead and try it, hit 'b' and type a command, e.g. 'ls'. - -Note that when you hit 'b' you will not see any change in the output, -but you can now start typing freely. - -``` -echo "This is a dummy code block to ensure SimDem pauses in interactive mode" -``` - -# Repeating Commands - -Sometimes a command will need to be run a number of times, for example -it might be monitoring the state of an operation. The easiest way to -repeat a command is simply to press 'r'. - -``` -echo "This is a dummy code block to ensure SimDem pauses in interactive mode" -``` - -# Next Steps - - 1. [Use your documents as automated tests](../test/README.md) - 2. [SimDem Index](../README.md) - 3. [Modes of operation](../modes/README.md) - 4. [Build a SimDem container](../building/README.md) diff --git a/demo_scripts/simdem/special_commands/README.md b/demo_scripts/simdem/special_commands/README.md deleted file mode 100644 index f9dbcb6..0000000 --- a/demo_scripts/simdem/special_commands/README.md +++ /dev/null @@ -1,37 +0,0 @@ -# Special Commands - -Some commands will be intercepted by SimDem and handled as special -commands. For example, we might interccept a command to open a browser -at a specific page and handle it differently in a headless CLI -environment to how it is handled in a Web UI environment. - -In fact lets look at that use case as an example. - -## Opening a Browser Tab - -On linux the command `xdg-open` is the accepted way of opening a -browser, therefore it is the accepted way of having such a command in -SimDem script. However, this poses a problem, behoviour of this -command will be different in different UI environments. - -For example, on a headliess CLI environment it will attempt to open -"lynx" or similar text based browsers. Since these are interactive -programs they will not work in SimDem. If running in a desktop -environment it will attempt to open the preferred browser. - -SimDem will intercept this command and handle it appropriately. That -is, in a headless CLI environment it will convert the command to a -"curl -I" command, this at least allows us to ensure that there is a -resposne from the URL provided. When running in a Web UI it will open -a new browser tab (at least at the time of writing, we may decide to -integrate this with the Web UI at some point). - -### In Action - -The command block below contains the `xdg-open` command, depending on -whether you are running in the Web UI or the CLI you will see -different behaviour, as described above. - -``` -xdg-open http://bing.com -``` diff --git a/demo_scripts/simdem/syntax/README.md b/demo_scripts/simdem/syntax/README.md deleted file mode 100644 index 8098683..0000000 --- a/demo_scripts/simdem/syntax/README.md +++ /dev/null @@ -1,120 +0,0 @@ -# Document syntax - -For the most part SimDem uses standard -[Markdown syntx](https://daringfireball.net/projects/markdown/syntax). There -are a few special strings that can be used to influence how SimDem -works, but even these are intended to be human readable, thus -preventing the need to maintain separate documents for the different -use cases (documentation, tutorial, demo, test and script). - -For example, lets take a look at the start of this file this file: - -``` -head -n 25 README.md -``` - -# Prerequisites - -It is common for a tutorial or demo to have a number of -prerequisites. For example, it's a good idea to understand how these -work, so take a look at our [prerequisites](../prerequisites/README.md) -before proceeding. - -# SimDem Syntax - -There are a small number of SimDem specific items that you should be -aware of. They are detailed in the next few sections. - -## Code Blocks - -In SimDem a code block is marked in exactly the same way it is in -Markdown, that is with three backticks (``````). Unless a Code Block -is marked as a Results block (see next section) it is assumed that -this is executable code. SimDem will execute each line individually. - -For example: - -``` -# This is a code block, this comment will be ignored by SimDem -echo "This command will be 'typed' and executed." -``` - -### Command Limitations - -At the time of writing it is not possible to have interactive -commands. If you try to include such a command SimDem will "hang" as -it waits, silently, for input. - -## Result Blocks - -Result blocks serve two main purposes: - - 1. Help readers of the markdown content understand expected behaviour - 2. Enable SimDem documents to be used as tests - -### Including results blocks for readablity and testing - -When using the script as a web page or printout it is likley that you -will want to include the results. However, when you are running in a -simulation or tutorial mode you will want to rely on the real results -from the current run. You can include a "Results:" section after any -code block. The first code block after this text will be ignored when -running in an interactive mode (such as tutorial or simulation). That -is, in the example below, the `date -u` command will be run -interactively but the `Sat Mar 12 10:09:12 UTC 2016` will only be -included in a static form of the script. - -``` -date -u -``` - -Results: - -```expected_similarity=0.4 -Sat Mar 12 10:09:12 UTC 2016 -``` - -### Modifying Test Accuracy - -When running a script as a test the outputs of the command are -compared to the result block associated with the code block. By -default a similarity of 66%, meaning at least 66% of the characters -are the same, is considered a pass. However, in some cases this is too -high or too low. - -The expected similarity between the command output and the contents of -the result block can be set in the `Results:` header by adding -`Expected similarity: 0.2`, where `0.2` is the similarity desired. - -This can be used to ensure things that have low similarity in the results will pass tests, for example, outputing a date will always result in a different date and thus a much lower expected similarity. - -The date command will prove this is running in real time. - -``` -date -``` - -Results: - -```expected_similarity=0.4 -Sat Mar 12 08:59:01 UTC 2016 -``` - -## Defining Next Steps - -When running in interactive mode it is possible to provide optional -paths for the user to take next. These appear in a section with the -heading "# Next Steps". Note, for this to work as an interactive set -of options this must be the last section in the document. if it is not -the last section then it will be treated like any other heading. - -For example, this document offers next steps options. - -# Next Steps - - 1. [Special Commands](special_commands/README.md) - 2. [Configure your scripts through variables](../variables/README.md) - 3. [Build a Hello World script](../tutorial/README.md) - 4. [SimDem Index](../README.md) - 5. [Write multi-part documents](../multipart/README.md) - 6. [Use your documents as interactive tutorials or demos](../running/README.md) diff --git a/demo_scripts/simdem/test/README.md b/demo_scripts/simdem/test/README.md deleted file mode 100644 index 0f00162..0000000 --- a/demo_scripts/simdem/test/README.md +++ /dev/null @@ -1,89 +0,0 @@ -# Automated Testing - -When running with the `--test` flag or using the `test` command SimDem -will verify that the output of each command is as expected. It does -this by comparing the output of the command with the `Results:` -section in the script. - -Test mode is very useful in a continuous integration environment. For -example, you can configure your scripts to always use the latest -versions of tooling they depend upon and get early warning when a -change in one of those tools breaks your -scripts. The -[Azure Container Service demos GitHub repository](http://github.com/Azure/acs-demos) shows -this technique in use. - -## Testing in practice - -First, lets take a look at the source of this file. - -``` -cat $SIMDEM_CWD/README.md -``` - -## Running in test mode - -Running in demo mode will not check the results against -expectations. However, running with the `test` command will do so. - -``` -echo "This test is expected to fail" -``` - -Results: - -``` -It fails because the results we have in the script are significantly -different to the output of the command. -``` - -### Similarity tests - -By default a 66% or more match indicates a pass. However, in some -cases a much lower similarity is expected, for example, the output of -`date` will vary considerably each time it is run. In these situations -you can provide an expected similarity as part of the three backticks -that start a code block, for example ```expected_similarity=0.2 which -is low enough for the test to be recorded as a pass. Note, it is -important that you do not insert any spaces in this notation. - -``` -date -``` - -Results: - -```Expected_Similarity=0.2 -Tue Jun 6 15:23:53 UTC 2017 -``` - -## Fast Fail - -The default setting is for SimDem to stop the test run on the first -test failure. This can be overridden by setting the command line flag -`--fastfail` to any value other than `True`. - -## Test Plans - -It is often a good idea to split tests into separate files. SimDem -will allow you to do this by providing a `test_plan.txt` file. Each -line in this file is either a comment (lines starting with '#') or a -filename for a SimDem script to be used in testing. Each of these -files will be concatenated together to create a complete test plan. - -For example, the following example `test_plan.txt` will run all the -code and tests in `preparation/README.md` followed by those in -`main/README.md` and finally those in `cleanup/README.md`. - -` -preparation/README.md -main/README.md -cleanup/README.md -` - -# Next Steps - - 1. [SimDem Index](../README.md) - 2. [Build a Hello World script](../tutorial/README.md) - 3. [Write SimDem documents](../syntax/README.md) - 4. [Configure your scripts through variables](../variables/README.md) diff --git a/demo_scripts/simdem/tutorial/README.md b/demo_scripts/simdem/tutorial/README.md deleted file mode 100644 index e458dbc..0000000 --- a/demo_scripts/simdem/tutorial/README.md +++ /dev/null @@ -1,46 +0,0 @@ -# Writing a SimDem script - -This document desribes how to write a SimDem script. - -## Install SimDem - -These commands can't be run from within SimDem - danger of turning the -whole world into a recursive simulation of itself ;-). Therefore you -should execute them on your client (Linux, Mac or Windows Subsystem -for Linux). - -`git clone git@github.com:rgardler/simdem.git` -`pushd simdem` -`./install.sh` -`simdem` - -This last command will launch you into the SimDem documentation. If -you haven't reviewed it already you should do so, of particular -importance is the Syntax section. - -## Hello World Script - -Let's build a hello world script: - -``` -mkdir -p hello_world -echo "# Hello World Script" > hello_world/README.md -``` - -## Run the script - -You can't run SimDem within SimDem so if you are reading this from -within SimDem you will need to exit and run `simdem -p hello_world` to -see this in action. - -# Next Steps - -Now you have a working hello world script you are on your own (not -really ask questions / report bugs via the -http://github.com/rgardler/simdem issue tracker). If you feel a little -lost then try one of these documents for guidance: - - 1. [Review SimDem Syntax](../syntx/README.md) - 2. [Understand how to parameterize scripts](../variables/README.md) - 3. [Understand how to make a script a test](../test/README.md) - diff --git a/demo_scripts/simdem/variables/README.md b/demo_scripts/simdem/variables/README.md deleted file mode 100644 index 62f4bd2..0000000 --- a/demo_scripts/simdem/variables/README.md +++ /dev/null @@ -1,168 +0,0 @@ -# Environment Variables - -In order to use environment variables, you can define one or more -files. These variables are available in every command that is -executed. - -Tutorials can carry `env.json` files in the directory the simdem -command was run and/or in tutorial sub-directories. Files in tutorial -sub-directories will overwrite settings pulled from the project -directory. - -For example, this tutorial defines an 'env.json' in the `simdem` -parent folder and in the `variables` subdirectory that contains this -script. Here is the content from the test subdirectory. - -``` -cat $SIMDEM_CWD/env.json -``` - -Results: - -``` -{ - "NAME": "Hello from the variables subdirectory" -} -``` - -It also defines an 'env.json' file in the `SimDem` root -folder. Assuming you executed the `simdem` command from within that -folder the followin command will display it's content. - -``` -cat env.json -``` - -Results: - -``` -{ - "TEST": "A local hello from the current working directory (where the simdem command was executed)" -} -``` - -Finally, a project may define an `env.local.json` file in the -directory from which the `simdem` command is run. This file is the -last to be loaded and overrides all other values. - -Values are loaded in the following order, the last file to define a - vlaue is the one that "wins". - - - PARENT_OF_SCRIPT_DIR/env.json - - SCRIPT_DIR/env.json - - PARENT_OF_SCRIPT_DIR/env.local.json - - SCRIPT_DIR/env.local.json - - CWD/env.json - - CWD/env.local.json - - -## Interactive Variables - -If you include an environment variable that isn't set, SimDem will -prompt you to give it a value and will add it to the running -environment. If you are running in test mode the variable will be -given a value of 'Dummy vlaue for test'. - -``` -echo $NEW_VARIABLE -``` - -Results: - -``` Expected_Similarity=0 -Enter a value for $NEW_VARIABLE: SimDem - -SimDem - -``` - -### Defining variables can be important - -Because SimDem will interactively ask for values for undefined -variables it is sometimes necessary to first declare a variable to -prevent this action. For example: - -``` -i=0 -for i in {0..4}; do echo "Welcome $i times"; done -``` - -Results: - -``` -Welcome 0 times -Welcome 1 times -Welcome 2 times -Welcome 3 times -Welcome 4 times -``` - -## User provided environment - -Since it is helpful to provide configuration files in published -scripts SimDem also provides a way for users to provide user specific -configurations. So that users can setup their demo's to use private -keys etc. These files are provided in the same way as `env.json` files -(i.e. in the project and tutorial sub-directories) but are called -`env.local.json`. These files take precedence over both project and -tutorial provided files. - -For example, this project provides a local files in both the project -and this tutorial sub-directories. Note that in this case we have -checked them into version control as they are part of the example, -normally they would be added to your local '.gitignore' or equivalent. - -``` -cat $SIMDEM_CWD/../env.local.json -``` - -Results: - -``` -{ - "LOCAL_TEST": "Hello from the local project config" -} -``` - -It also defines an 'env.json' file in the tutorial folder: - -``` -cat $SIMDEM_CWD/env.local.json -``` - -Results: - -``` -{ - "LOCAL_TEST": "A warm local hello" -} -``` - -The file that "wins" is the most local one, that is the one in the tutorial: - -``` -echo $LOCAL_TEST -``` - -Results: - -``` -A warm local hello -``` - -## SimDem Environment Variables - -SimDem provides some information about itself in environment -variables. These are all nameed `SIMDEM_*`. At present the available -variables are: - -``` -env | grep "SIMDEM_" -``` - -# Next Steps - - 1. [Build a SimDem container](../building/README.md) - 2. [SimDem Index](../README.md) - 3. [Use your documents as interactive tutorials or demos](../running/README.md) - 4. [Use your documents as automated tests](../test/README.md) diff --git a/demo_scripts/simdem/variables/env.json b/demo_scripts/simdem/variables/env.json deleted file mode 100644 index b76ccd5..0000000 --- a/demo_scripts/simdem/variables/env.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "NAME": "Hello from the variables subdirectory" -} diff --git a/demo_scripts/simdem/variables/env.local.json b/demo_scripts/simdem/variables/env.local.json deleted file mode 100644 index e6c7c04..0000000 --- a/demo_scripts/simdem/variables/env.local.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "LOCAL_TEST": "A warm local hello" -} diff --git a/demo_scripts/test/README.md b/demo_scripts/test/README.md deleted file mode 100644 index bfe00c2..0000000 --- a/demo_scripts/test/README.md +++ /dev/null @@ -1,157 +0,0 @@ -# SimDem Test Script - -This is a simple test script. It runs a number of commands in -succession. This script also lists commands known not to work. - -# Setup - -Ensure the test environment is correctly setup. - -## SimDem version check - -``` -echo $SIMDEM_VERSION -``` - -Results: - -```expected_similarity=0.8 -0.8.2-dev -``` - -## Clean test working files - -Ensure that our working files folder exists and that there are no -residual files from previus test runs. - -``` -echo $SIMDEM_TEMP_DIR -mkdir -p $SIMDEM_TEMP_DIR/test -rm -Rf $SIMDEM_TEMP_DIR/test/* -``` - -# Prerequisites - -Test to see if our prerequesites work. In the setup we cleaned out our -test files. The [prerequisite test script](./prerequisites/README.md) -validates whether the file exists and, if it doesn't it will execute -and create it. - -Each [prerequisite](./prerequisites/README.md) will only be run once, -so even though this partucular prereq appears twice it will only -execute once. This is important when building multi-part tutorials/ -demos where a prereq may be included in more than one part. - -## Validate prerequisite ran - -The prerequisite script should have run and created a `prereq_ran` -file. - -``` -ls $SIMDEM_TEMP_DIR/test -``` - -Results: - -``` -prereq_ran -``` - - -# Directory Check - -``` -head -n 1 README.md -``` - -Results: - -``` Expected_Similarity=0.8 -# SimDem Test Script -``` - -# Simple Echo - -``` -echo "Hello world" -``` - -Results: - -``` -Hello world -``` - -# Code comments - -``` -# This is a comment and should be ignored -echo "This output should be displayed, the comment before this line should be ignored" -``` - -Results: - -``` -This output should be displayed, the comment before this line should be ignored -``` - -# Expected different results - -When we know the results will be different and we want to use them in -tests we need to override the similarity expected by adding -`expected_similarity=x.y` in the start line of the results block: - -``` -date -``` - -Results: - -```expected_Similarity=0.2 -Tue Jun 6 15:23:53 UTC 2017 -``` - -# For Loop - -Because SimDem will interactively ask for values for undefined -variables it is sometimes necessary to first declare a variable to -prevent this action. For example: - -``` -i=0 -for i in {0..10}; do echo "Welcome $i times"; done -``` - -Results: - -``` -Welcome 0 times -Welcome 1 times -Welcome 2 times -Welcome 3 times -Welcome 4 times -Welcome 5 times -Welcome 6 times -Welcome 7 times -Welcome 8 times -Welcome 9 times -Welcome 10 times -``` - -# Stripping ANSI escape sequances - -To make it easier to write scripts we don't want to include ANSI -escape sequences (such as colors and text deocration) in the results -section. SimDem automatically strips these when capturing the results. - -``` -printf "Normal \e[4mUnderlined\e[24m Normal" -``` - -Results: - -```expected_similarity=0.9 -Normal Underlined Normal -``` - - diff --git a/demo_scripts/test/directory/README.md b/demo_scripts/test/directory/README.md deleted file mode 100644 index a8443dc..0000000 --- a/demo_scripts/test/directory/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# Directory Check - -In this test ensures that the currrent working directory is set -correctly when a test file is loaded as part of the test_plan.txt -file, see [Issue #70](https://github.com/Azure/simdem/issues/70). - -Frst lets check the current working directory, this is useful for -debugging if the test fails. - -``` -pwd -``` - -Since we don't know exactly where this will be stored we need to check -that we can open this file in the test. - -``` -head -n 1 README.md -``` - -Results: - -``` Expected_Similarity=0.8 -# Directory Check -``` - diff --git a/demo_scripts/test/env.json b/demo_scripts/test/env.json deleted file mode 100644 index 8bfd5a8..0000000 --- a/demo_scripts/test/env.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "TEST": "Hello from the test script", - "DIR_IN_HOME": "~/should/be/expanded" -} diff --git a/demo_scripts/test/env.local.json b/demo_scripts/test/env.local.json deleted file mode 100644 index 3d9ee80..0000000 --- a/demo_scripts/test/env.local.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "TEST": "A local hello from the current working directory (where the simdem command was executed)" -} diff --git a/demo_scripts/test/env.test.json b/demo_scripts/test/env.test.json deleted file mode 100644 index 948b4f7..0000000 --- a/demo_scripts/test/env.test.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "TEST_VALUE": "Test value for the test script" -} diff --git a/demo_scripts/test/environment_test.md b/demo_scripts/test/environment_test.md deleted file mode 100644 index c7bde24..0000000 --- a/demo_scripts/test/environment_test.md +++ /dev/null @@ -1,195 +0,0 @@ -# Environment tests - -We should be able to retrieve environment variables from the directory -in which the command was given. Note that SimDem provides the -environment variable `SIMDEM_EXEC_DIR` which provides access to this -folder in SimDem scripts should it be necessary. - -``` -cat $SIMDEM_EXEC_DIR/./env.json -``` - -Results: - -``` -{ - "TEST": "Hello from the current working directory (where the simdem command was executed)" -} -``` - -We should also be able to retrieve locallay defined environment -variables from the directory in which the command was given: - - -``` -cat env.local.json -``` - -Results: - -``` -{ - "TEST": "A local hello from the current working directory (where the simdem command was executed)" -} -``` - -There should also be environment variables in the the directory in -which the current script resides. - -``` -cat env.json -``` - -Results: - -``` -{ - "TEST": "Hello from the test script", - "DIR_IN_HOME": "~/should/be/expanded" -} -``` - -Local variables can also be found in the the directory in which the -current script resides. - -``` -cat env.local.json -``` - -Results: - -``` -{ - "TEST": "A local hello from the current working directory (where the simdem command was executed)" -} -``` - -For the `TEST` variable we should have the `env.local.json` value from -the directory in which the application was executed. - -``` -echo $TEST -``` - -Results: - -``` -A local hello from the current working directory (where the simdem command was executed) -``` - -There should be variable definitions in the parent of the -current script directory: - -``` -cat ../env.json -``` - -Results: - -``` -{ - "PARENT_TEST": "Hello from the parent" -} -``` - -Since the value of `PARENT_TEST` is only defined in this file we -should have the value from there: - -``` -echo $PARENT_TEST -``` - -Results: - -``` -Hello from the parent -``` - -## Test Environment - -We can also provide values in `env.test.json` in either the script -directory or the parent of the script directory. If available these -will be loaded first and overwritten by subsequent `env.json` and -`env.local.json` files. For this reason if you want to dorce the user -to provide a value for an environment variable it is important that -you define it as an empty string in `env.json` if a value has been -provided in `env.test.json`. - - -``` -echo $TEST_VALUE -``` - -Results: - -``` -Test value for the test script -``` - -# Replacing variables in special commands - -Some commands cannot be run in headless mode, e.g. `xdg-open`. These -commands will be replaced with an appropriate alternative, -e.g. `curl`. Variables included in such commands will be expanded at -execution time as expected. - -``` -url=http://bing.com -xdg-open $url -``` - -Results: - -```expected_similarity=0.2 -HTTP/1.1 405 Method Not Allowed -Content-Length: 0 -Server: Microsoft-IIS/10.0 -X-MSEdge-Ref: Ref A: D6871D12117C436FAE3762BC8BCA0C29 Ref B: CO1EDGE0415 Ref C: 2017-08-17T15:37:59Z -Date: Thu, 17 Aug 2017 15:37:58 GMT -``` - -Note: this test will only pass when running in headless mode as the -`xdg-open` command will be executed in other environments. - -# Setting new variables in script - -If a script sets a variable during execution this will be recorded in -the SimDem environment. This includes setting to an empty string, this -will prevent SimDem interactively requesting a value for the variable -(or setting a dummt value in test mode). - -``` -new_var="" -echo $new_var -``` - -Results: - -```expected_similarity=0.2 -``` - -# Capturing the output of commands - -``` -CAPTURED_OUTPUT=$(echo foo | sed 's/foo/bar/') -``` - -Captured value is: - -``` -echo $CAPTURED_OUTPUT -``` - -Results: - -``` -bar -``` - -# Processing of Environment Variables - -'~' should be expanded to a home directory (no way to test this). - -``` -echo $DIR_IN_HOME -``` diff --git a/demo_scripts/test/prerequisites/README.md b/demo_scripts/test/prerequisites/README.md deleted file mode 100644 index 969792d..0000000 --- a/demo_scripts/test/prerequisites/README.md +++ /dev/null @@ -1,33 +0,0 @@ -# Test Prerequisites - -This script is not included as part of the test plan, but it should be -executed as part of the root `script.md`. Therefore, because of this -prerequisite there should be a file called `prereq_ran` and another -called `nested_prereq_ran` in the temp directory. - -# Prerequisites - -We should be able to run [nested prerequisites](./nested_prereq.md). - -# Create the test file - -``` -touch $SIMDEM_TEMP_DIR/test/prereq_ran -``` - -# Validation - -If the `prereq_ran` file exists then we don't need to run this -script. In our tests the setup phase removes this file so the -validation test should always fail. - -``` -ls $SIMDEM_TEMP_DIR/test -``` - -Results: - -``` -nested_prereq_ran -prereq_ran -``` diff --git a/demo_scripts/test/prerequisites/nested_prereq.md b/demo_scripts/test/prerequisites/nested_prereq.md deleted file mode 100644 index 2876fcf..0000000 --- a/demo_scripts/test/prerequisites/nested_prereq.md +++ /dev/null @@ -1,25 +0,0 @@ -# Nested Prerequisites - -This prerequisite is executed from the main prerequisite test file. - -# Create the test file - -``` -touch $SIMDEM_TEMP_DIR/test/nested_prereq_ran -``` - -# Validation - -If the `prereq_ran` file exists then we don't need to run this -script. In our tests the setup phase removes this file so the -validation test should always fail. - -``` -ls $SIMDEM_TEMP_DIR/test -``` - -Results: - -``` -nested_prereq_ran -``` diff --git a/demo_scripts/test/prerequisites/passing_prerequisite.md b/demo_scripts/test/prerequisites/passing_prerequisite.md deleted file mode 100644 index 3dcd455..0000000 --- a/demo_scripts/test/prerequisites/passing_prerequisite.md +++ /dev/null @@ -1,28 +0,0 @@ -# Passing Prerequisite - -This is a dummy prerequisite file that contains a validation step that -will always pass and thus the body of this script will never be -executed. To ensure this is the case when we run tests we have placed -a failing test in the body. - -``` -echo "This test will always fail" -``` - -Results: - -``` -So we can ensure it never runs (the validation step will always pass) -``` - -# Validation - -``` -echo "This validation step is designed to always pass" -``` - -Results: - -``` -This validation step is designed to always pass -``` diff --git a/demo_scripts/test/remote/README.md b/demo_scripts/test/remote/README.md deleted file mode 100644 index f3647df..0000000 --- a/demo_scripts/test/remote/README.md +++ /dev/null @@ -1,33 +0,0 @@ -# A remote script - -This file will be pulled from GitHub as a remote file to ensure that -execution of remote files works. It is pulled into the test suite in -two ways: - - 1. As a remote prerequisite - 2. AS a remote file in a test plan - -# Create the test file - -``` -mkdir -p $SIMDEM_TEMP_DIR/test -touch $SIMDEM_TEMP_DIR/test/remote_prereq_ran -``` - -# Validation - -If the `remote_prereq_ran` file exists then we don't need to run this -script. In our tests the setup phase removes this file so the -validation test should always fail. - -``` -ls $SIMDEM_TEMP_DIR/test -``` - -Results: - -``` -nested_prereq_ran -prereq_ran -remote_prereq_ran who -``` diff --git a/demo_scripts/test/test_plan.txt b/demo_scripts/test/test_plan.txt deleted file mode 100644 index d482ad4..0000000 --- a/demo_scripts/test/test_plan.txt +++ /dev/null @@ -1,4 +0,0 @@ -# SimDem Test Plan -README.md -directory/README.md -environment_test.md diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..207737c --- /dev/null +++ b/docs/README.md @@ -0,0 +1,34 @@ +# Welcome to SimDem + +SimDem is: + +* Documentation +* An interactive tutorial +* A live demo +* An automated test script +* A Shell script + +## SimDem overview + +Simdem allows you to wite a tutorial in markdown format and then run the commands as a simulated demo, interactive tutorial, a test script or generate executable shell scripts. + +SimDem reads a Markdown file and executes the code block commands embedded within. It can even look like you are really typing the commands, which is great if you want to concentrate on explaining what you are doing but still run the demo live. + +It's easier to understand through example. If you are viewing this inside the interactive demo, press a key (other than 'b', we'll look at that shortly) to "type" a command, once the command has been "typed" hit a key to execute the command. + +# Next Steps + +Tutorials can branch too, for example you can choose any of the following paths next: + +1. [Modes of operation](modes.md) +1. [Build Hello World Demo](hello_world.md) +1. [SimDem document syntax](syntax.md) +1. [Use your documents as an interactive tutorial](mode_tutorial.md) +1. [Use your documents as an interactive demo](mode_demo.md) +1. [Use your documents as an automated test](mode_test.md) +1. [Features](features.md) +1. [Advanced Features](feature_advanced.md) + + + + diff --git a/demo_scripts/simdem/demo/README.md b/docs/demo.md similarity index 89% rename from demo_scripts/simdem/demo/README.md rename to docs/demo.md index c46c51a..249624e 100644 --- a/demo_scripts/simdem/demo/README.md +++ b/docs/demo.md @@ -15,7 +15,7 @@ echo "Hello World" That's cool, lets try again: ``` -echo "It might look like this was typed into the terminal (even more so if you ran SimDem with the '--style simulate' flag, that simulates a person typing), bit it really comes from a markdown file." +echo "It might look like this was typed into the terminal, but it really comes from a markdown file." ``` The date command will show that these commands are being executed in real time. diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 0000000..07dd33e --- /dev/null +++ b/docs/development.md @@ -0,0 +1,69 @@ +# Preface + +We would love for you to contribute to SimDem. If there is anything that would make adoption/developing of SimDem easier, please open up an issue with your request. + +# Setup Dev Environment + +## Prerequisites + +* python3 +* pip3 +* Linux shell + +## Initialize + +Fetch the necessary packages + +``` +pip3 install -r requirements.txt +``` + +## Validation + +Verify the tests pass + +``` +python3 setup.py nosetests +``` + +# Code Structure + +SimDem is broken into the following class types + +## Parse + +This class type parses the markdown document into a "SimDem Execution Object". This object has everything SimDem needs to know on how to run the document (e.g. prerequisites, commands, validations, etc.) + +Follow the links to see an [example document](../content/prerequisites/README.md) with its output [SimDem Execution Object](../content/prerequisites/expected_output.dump) + +Implementations: +* [SimDem1](../simdem/parser/simdem1.py) + +## Mode + +This class contains the logic for how the markdown document is processed. + +Implementations: +* [Demo mode](../simdem/mode/demo.py) +* [Automated mode](../simdem/mode/automated.py) +* [Dump](../simdem/mode/dump.py) +* [Tutorial](../simdem/mode/tutorial.py) + +## Execute + +This class type executes the desired commands into the shell + +Implementations: +* [Bash](../simdem/executor/bash.py) + +## Brittle testcases + +Because I'm a bad developer and the test cases for SimDem are brittle, here's how to regenerate all of the .demo/.tutorial files. + +This might happen because adding a newline to the logic breaks the testcases. +``` +for dir in simple simple-variable results-block results-block-fail prerequisites; do simdem -m tutorial examples/$dir/README.md > examples/$dir/expected_result.tutorial; done; +for dir in simple simple-variable results-block results-block-fail prerequisites; do simdem -m dump examples/$dir/README.md > examples/$dir/expected_result.seo; done; +for dir in simple simple-variable results-block results-block-fail prerequisites; do simdem -m demo examples/$dir/README.md > examples/$dir/expected_result.demo; done; + +``` \ No newline at end of file diff --git a/docs/feature_advanced.md b/docs/feature_advanced.md new file mode 100644 index 0000000..eb0be63 --- /dev/null +++ b/docs/feature_advanced.md @@ -0,0 +1,15 @@ +# Advanced Features + +## Setup Script + +A setup script is a simple way of bootstrapping your environment prior to running the document. + +You might use this feature if you want to do the following before running the main documentation: +* Set environment variables +* Script prerequisite commands + +Example usage: + +```shell +simdem -s examples/setup-script/setup.sh examples/setup-script/README.md +``` \ No newline at end of file diff --git a/demo_scripts/simdem/prerequisites/README.md b/docs/feature_prerequisite.md similarity index 88% rename from demo_scripts/simdem/prerequisites/README.md rename to docs/feature_prerequisite.md index d0bd278..34c3fac 100644 --- a/demo_scripts/simdem/prerequisites/README.md +++ b/docs/feature_prerequisite.md @@ -19,6 +19,18 @@ should be run ahead of the current one. The scripts should appear in the order of required exection in the body. +# Behavior + +When a prerequisite script is identified SimDem will ask the user if +they have satisfied the requirement. If SimDem is running in test or +auto mode it is assumed that prerequisites have been satisified. + +If the user indicates a prerequisite has been satisfied then execution +moves to the next prerequisite or onto the rest of the script. + +If the user indicates a prereqiusite has not been satisfied then the +required script is executed. + # Automatically validating Pre-requisites Some pre-requisite steps can take a long time to execute. For this @@ -58,7 +70,7 @@ otherwise the other commands in the script will be executed. # Validation -In order to continue with our example we include some vlaidation steps +In order to continue with our example we include some validation steps in this script. If you have not run through the commands above less than one minute ago this validation stage will fail. If you are working through this tutorial now you just executed the above diff --git a/docs/feature_result.md b/docs/feature_result.md new file mode 100644 index 0000000..c8756e4 --- /dev/null +++ b/docs/feature_result.md @@ -0,0 +1,38 @@ +# Command Testing + +An example of a Result test is: + +``` +echo "This test is expected to fail" +``` + +Results: + +``` +It fails because the results we have in the script are significantly +different to the output of the command. +``` + +By default a 66% or more match indicates a pass. However, in some +cases a much lower similarity is expected, for example, the output of +`date` will vary considerably each time it is run. In these situations +you can provide an expected similarity as part of the three backticks +that start a code block, for example ```Expected_Similarity=0.2 which +is low enough for the test to be recorded as a pass. Note, it is +important that you do not insert any spaces in this notation. + +``` +date +``` + +Results: + +```Expected_Similarity=0.2 +Tue Jun 6 15:23:53 UTC 2017 +``` + +` +preparation/README.md +main/README.md +cleanup/README.md +` diff --git a/docs/features.md b/docs/features.md new file mode 100644 index 0000000..7709129 --- /dev/null +++ b/docs/features.md @@ -0,0 +1,41 @@ +# Features + +This document is intended to be a list of all features supported in SimDem. To see examples on how to write documents that use these features, please see the [syntax documentation](syntax.md). + + +## Commands +* Execution - Command extracted from the document to be run in a shell + * Shell - All command are run in a Linux shell. (PR for Powershell is welcome) + * Comments - If command starts with `#`, it will be considered a comment and will not be run +* Results - Validate if the previous command ran correctly. See validation section for details + * If validation passes - Continue processing + * If validation fails - Exit program (default) +* Strip ANSI escape sequences + +## Environment Variables + * Allow the ability to inject environment variables via file or CLI (key/value format) + +## Prerequisites +* Allows ability to link to other SimDem documents to run prior to main execution. (e.g. Need Azure CLI setup before creating resource group.) Will be only run once. +* Validation - Validate if the prequisite requirements have been met. See validation section for details + * If validation passes - Don't run prerequisite document + * If validation fails - Continue to run prerequsite document + +For details, see the [prerequisite feature documentation](feature_prerequisite.md) + +## Interrupt running + * When running interactively, allow the ability to interrupt running document to run manual commands. Resume processing document when complete + +## Validation + +Validation is used to verify that the expected result is matched. The two main uses for it are for Command Execution and Prerequisites. + + * Allows the output of the previous code block to be evaluated to determine if it meets an expected result. Validation is either passed/failed. + +### Validation types + * Expected Result Similarity - e.g. .80 match + * Result Exact Match + * Result Pattern Match + * Exit code + +For details, see the [validation feature documentation](feature_validation.md) diff --git a/docs/fin.md b/docs/fin.md new file mode 100644 index 0000000..6482e96 --- /dev/null +++ b/docs/fin.md @@ -0,0 +1,5 @@ +# Fin + +```shell +cowsay "Thanks for trying SimDem! https://github.com/Azure/simdem/tree/simdem2" +``` diff --git a/docs/hello_world.md b/docs/hello_world.md new file mode 100644 index 0000000..d6c16af --- /dev/null +++ b/docs/hello_world.md @@ -0,0 +1,34 @@ +# Hello World + +This is the start of a hello-world example. + +## Running commands + +To run a set of commands, insert them inside a code block. Here's an example: + +NOTE: If running in tutorial or demo mode, you will need to press a key to tell SimDem to execute the command + +```shell +echo "hello world" +``` + +To verify output, add a "Results:" paragraph immediately after your command: + +```shell +echo "hello again" +``` + +Results: + +``` +hello again +``` + +# Next Steps + +Tutorials can branch too, for example you can choose any of the following paths next: + +1. [SimDem document syntax](syntax.md) +1. [Use your documents as interactive tutorials or demos](mode_demo.md) +1. [Use your documents as automated tests](mode_test.md) +1. [FIN](fin.md) diff --git a/docs/mode_demo.md b/docs/mode_demo.md new file mode 100644 index 0000000..efbf5ae --- /dev/null +++ b/docs/mode_demo.md @@ -0,0 +1,9 @@ +# Demo (or Simulation) mode + +Demo mode is ideal if you are using this to teach or demonstrate how +to achive the goal. In this mode no descriptive text is shown, instead +when you press a key the next command is "typed", pressing another key +will execute the command. The idea is that you describe what is +happening as the application "types" the command for you. + +To run a document in Demo Mode: `simdem -m demo examples/simple/README.md` diff --git a/docs/mode_dump.md b/docs/mode_dump.md new file mode 100644 index 0000000..bdcda84 --- /dev/null +++ b/docs/mode_dump.md @@ -0,0 +1,5 @@ +# Dump Mode + +Dump mode is used for debugging to see how SimDem has parsed the document + +To run a document in Dump Mode: `simdem -m dump examples/simple/README.md` diff --git a/docs/mode_test.md b/docs/mode_test.md new file mode 100644 index 0000000..23dbd08 --- /dev/null +++ b/docs/mode_test.md @@ -0,0 +1,8 @@ +# Test Mode + +Test mode runs the commands and then verifies that the output is +sufficiently similar to the expected results (recorded in the markdown +file) to be considered correct. + +To run a document in Test Mode: `simdem -m test examples/simple/README.md` + diff --git a/docs/mode_tutorial.md b/docs/mode_tutorial.md new file mode 100644 index 0000000..d14f74d --- /dev/null +++ b/docs/mode_tutorial.md @@ -0,0 +1,9 @@ +# Tutorial Mode + +Tutorial mode is ideal if you are using this as a learning or teaching +tool (see also learn mode below, which suits some learning styles +better. In this mode a description of what you are about to do is +shown on the screen, hit a key to see the command, hit another key to +execute the command. Tutorial mode is the default. + +To run a document in Tutorial Mode: `simdem examples/simple/README.md` diff --git a/docs/modes.md b/docs/modes.md new file mode 100644 index 0000000..c4da7d8 --- /dev/null +++ b/docs/modes.md @@ -0,0 +1,23 @@ +# Modes of Operation + +SimDem and can be run one of the following modes: + +* Tutorial: Displays the descriptive text of the tutorial and pauses + at code blocks to allow user interaction. +* Demo: Does not display the descriptive text, but pauses at each + code block. When the user hits a key the command is "typed", a + second keypress executes the command. +* Test: Runs the commands and then verifies that the output is + sufficiently similar to the expected results (recorded in the + markdown file) to be considered correct. +* Dump: Prints out the internal SimDem object + +# Next Steps + + 1. [Beginning](README.md) + 1. [Build Hello World Demo](hello_world.md) + 1. [SimDem document syntax](syntax.md) + 1. [Use your documents as an interactive tutorial](mode_tutorial.md) + 1. [Use your documents as an interactive demo](mode_demo.md) + 1. [Use your documents as an automated test](mode_test.md) + 1. [Dump your parsed documentation](mode_dump.md) \ No newline at end of file diff --git a/docs/syntax.md b/docs/syntax.md new file mode 100644 index 0000000..b3cdbda --- /dev/null +++ b/docs/syntax.md @@ -0,0 +1,20 @@ +# Syntax + +One of the designs of SimDem is to allow multiple implementations of Markdown to support different use cases and documentation patterns. + +Currently, there is only one implementation of Markdown syntax supported. + +# Context Syntax Specification + +This is the syntax for the default codeblock format. It's design is to allow more natural, expressive, and readable documentation. It is based off of SimDem v1's syntax. + +## SimDem V1 Based (default) + +[Example Context Based Document](../examples/simdem1/README.md) + +Feature | Implementation +--- | --- +Command | \```shell +[Prerequisite](feature_prerequisite.md) | `# Prerequisite` followed by natural language text containing links to local or remote SimDem documents taht that should be executed prior to the main body of the current document. See [Prerequisites](https://github.com/Azure/simdem/tree/master/demo_scripts/simdem/prerequisites) for more details. +[Validation](feature_validation.md) | `# Validation` followed by descriptive natural language and code-blocks to be run as tests prior to running the main content in this document. If all tests pass then there is no need to run the main document. See the [validation](https://github.com/Azure/simdem/tree/master/demo_scripts/simdem/prerequisites#validation) section of the prerequisites documentation for more details. +Result | `# Result` followed by a code block with the expected result of the code block immediately before the Results header. See the [testing](https://github.com/Azure/simdem/tree/master/demo_scripts/simdem/test) documentation for more details. diff --git a/env.json b/env.json deleted file mode 100644 index 3d9ee80..0000000 --- a/env.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "TEST": "A local hello from the current working directory (where the simdem command was executed)" -} diff --git a/env.local.json b/env.local.json deleted file mode 100644 index bbcace7..0000000 --- a/env.local.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "TEST": "A local hello from the current working directory (where the simdem command was executed)" -} - diff --git a/environment.py b/environment.py deleted file mode 100644 index 06cbb62..0000000 --- a/environment.py +++ /dev/null @@ -1,155 +0,0 @@ -# For managing the environment in which a SimDem demo executes. - -import os -import sys -import json - -import config - -class Environment(object): - def __init__(self, directory, copy_env=True, is_test=False): - """Initialize the environment""" - if copy_env: - self.env = os.environ.copy() - else: - self.env = {} - self.is_test = is_test - self.read_simdem_environment(directory) - self.set("SIMDEM_VERSION", config.SIMDEM_VERSION) - self.set("SIMDEM_CWD", directory) - self.set("SIMDEM_EXEC_DIR", os.getcwd()) - temp_dir = os.path.expanduser(config.SIMDEM_TEMP_DIR) - self.set("SIMDEM_TEMP_DIR", temp_dir) - - def read_simdem_environment(self, directory): - """Populates each shell environment with a set of environment vars - loaded via env.json and/or env.local.json files. Variables are - loaded in order first from the parent of the current script - directory, then the current scriptdir itself and finally from - the directory in which the `simdem` command was executed (the - CWD). - - Values are loaded in the following order, the last file to - define a vlaue is the one that "wins". - - - PARENT_OF_SCRIPT_DIR/env.json - - SCRIPT_DIR/env.json - - PARENT_OF_SCRIPT_DIR/env.local.json - - SCRIPT_DIR/env.local.json - - CWD/env.json - - CWD/env.local.json - - Note that it is possible to supply test values in an - `env.test.json` file stored in the SCRIPT_DIR, its parent or - the current working directory. If we are running in test mode - then the following three files will be loaded, if they exist, - in the following order at the end of the initialization - procedure. This means they will take precedence over - everything else. - - - PARENT_OF_SCRIPT_DIR/env.test.json - - SCRIPT_DIR/env.test.json - - CWD/env.json - - """ - env = {} - - if not directory.endswith('/'): - directory = directory + "/" - - filename = directory + "../env.json" - if os.path.isfile(directory + "../env.json"): - with open(filename) as env_file: - app_env = self.process_env(json.load(env_file)) - env.update(app_env) - - filename = directory + "env.json" - if os.path.isfile(filename): - with open(filename) as env_file: - script_env = self.process_env(json.load(env_file)) - env.update(script_env) - - filename = directory + "../env.local.json" - if os.path.isfile(filename): - with open(filename) as env_file: - local_env = self.process_env(json.load(env_file)) - env.update(local_env) - - filename = directory + "env.local.json" - if os.path.isfile(filename): - with open(filename) as env_file: - local_env = self.process_env(json.load(env_file)) - env.update(local_env) - - filename = os.getcwd() + "env.json" - if os.path.isfile(filename): - with open(filename) as env_file: - local_env = self.process_env(json.load(env_file)) - env.update(local_env) - - filename = os.getcwd() + "env.local.json" - if os.path.isfile(filename): - with open(filename) as env_file: - local_env = self.process_env(json.load(env_file)) - env.update(local_env) - - if self.is_test: - filename = directory + "../env.test.json" - if os.path.isfile(filename): - with open(filename) as env_file: - script_env = self.process_env(json.load(env_file)) - env.update(script_env) - - filename = directory + "env.test.json" - if os.path.isfile(filename): - with open(filename) as env_file: - script_env = self.process_env(json.load(env_file)) - env.update(script_env) - - filename = os.getcwd() + "/env.test.json" - if os.path.isfile(filename): - with open(filename) as env_file: - local_env = self.process_env(json.load(env_file)) - env.update(local_env) - - self.env.update(env) - - def process_env(self, new_env): - """ - Takes an environment definition and processes it for use. - For example, expand '~' to home directory. - """ - for key in new_env: - val = new_env[key] - if val.startswith('~'): - new_env[key] = os.path.expanduser(val) - return new_env - - def set(self, var, value): - """Sets a new variable to the environment""" - self.env[var] = value - - def get(self, key=None): - """Returns a either a value for a supplied key or, if key is None, a - dictionary containing the current environment""" - if key: - if key not in self.env: - return "UNDEFINED" - else: - return self.env[key] - else: - return self.env - - def dump_env(self): - """ - Prints the environment to the console. - """ - for item in self.env.items(): - print(str(item)) - - def __str__(self): - s = "" - for item in self.env.items(): - s += str(item) - s += "\n" - return s diff --git a/examples/config/demo.ini b/examples/config/demo.ini new file mode 100644 index 0000000..cd00d97 --- /dev/null +++ b/examples/config/demo.ini @@ -0,0 +1,19 @@ +[meta] +simdem_version = 0.9.0 + +[main] +temp_dir = .simdem + +# Logging +[log] +file = simdem.log +level = DEBUG +format = %(asctime)s [%(threadName)-12.12s] [%(levelname)-5.5s] %(message)s + +[render] +# When in demo mode we insert a small random delay between characters. +# TYPING DELAY is the upper bound of this delay. +typing_delay = 0.2 + +# Prompt to use in the console +console_prompt = $ \ No newline at end of file diff --git a/examples/config/unit_test.ini b/examples/config/unit_test.ini new file mode 100644 index 0000000..05bc13d --- /dev/null +++ b/examples/config/unit_test.ini @@ -0,0 +1,19 @@ +[meta] +simdem_version = 0.9.0 + +[main] +temp_dir = .simdem + +# Logging +[log] +file = simdem.log +level = DEBUG +format = %(asctime)s [%(threadName)-12.12s] [%(levelname)-5.5s] %(message)s + +[render] +# When in demo mode we insert a small random delay between characters. +# TYPING DELAY is the upper bound of this delay. +typing_delay = 0 + +# Prompt to use in the console +console_prompt = $ \ No newline at end of file diff --git a/examples/environment-files/README.md b/examples/environment-files/README.md new file mode 100644 index 0000000..d3d9795 --- /dev/null +++ b/examples/environment-files/README.md @@ -0,0 +1,9 @@ +# testing importing env.sh file + +text1 + +```shell +echo $FOO +``` + +text2 diff --git a/examples/environment-files/env.sh b/examples/environment-files/env.sh new file mode 100644 index 0000000..a4b9c23 --- /dev/null +++ b/examples/environment-files/env.sh @@ -0,0 +1,2 @@ +FOO=foo +BAR=bar \ No newline at end of file diff --git a/examples/environment-files/expected_result.demo b/examples/environment-files/expected_result.demo new file mode 100644 index 0000000..ed09d86 --- /dev/null +++ b/examples/environment-files/expected_result.demo @@ -0,0 +1,4 @@ +$ FOO=foo +$ BAR=bar +$ echo $FOO +foo diff --git a/examples/environment-files/expected_result.seo b/examples/environment-files/expected_result.seo new file mode 100644 index 0000000..97d5cad --- /dev/null +++ b/examples/environment-files/expected_result.seo @@ -0,0 +1,23 @@ +{ + "body": [ + { + "content": "testing importing env.sh file", + "level": 1, + "type": "heading" + }, + { + "content": "text1\n", + "type": "text" + }, + { + "content": [ + "echo $FOO" + ], + "type": "commands" + }, + { + "content": "text2\n", + "type": "text" + } + ] +} diff --git a/examples/environment-files/expected_result.tutorial b/examples/environment-files/expected_result.tutorial new file mode 100644 index 0000000..af3b1ca --- /dev/null +++ b/examples/environment-files/expected_result.tutorial @@ -0,0 +1,11 @@ +$ FOO=foo + +$ BAR=bar +# testing importing env.sh file + +text1 + +$ echo $FOO +foo +text2 + diff --git a/examples/markdown-syntax/README.md b/examples/markdown-syntax/README.md new file mode 100644 index 0000000..dc7716a --- /dev/null +++ b/examples/markdown-syntax/README.md @@ -0,0 +1,18 @@ +# Heading 1 + +## Code Block + +This is a code block: + +```shell +echo foo +echo bar +``` + +## Emphasis + +*This is emphasis* + +## Inline code + +Precode `this is inline code` Postcode \ No newline at end of file diff --git a/examples/markdown-syntax/expected_result.seo b/examples/markdown-syntax/expected_result.seo new file mode 100644 index 0000000..9853d2a --- /dev/null +++ b/examples/markdown-syntax/expected_result.seo @@ -0,0 +1,47 @@ +{ + "body": [ + { + "content": "Heading 1", + "level": 1, + "type": "heading" + }, + { + "content": "Code Block", + "level": 2, + "type": "heading" + }, + { + "content": "This is a code block:\n", + "type": "text" + }, + { + "content": [ + "echo foo", + "echo bar" + ], + "type": "commands" + }, + { + "content": "Emphasis", + "level": 2, + "type": "heading" + }, + { + "content": "*This is emphasis*", + "type": "text" + }, + { + "content": "\n", + "type": "text" + }, + { + "content": "Inline code", + "level": 2, + "type": "heading" + }, + { + "content": "Precode `this is inline code` Postcode", + "type": "text" + } + ] +} diff --git a/examples/next-steps/README.md b/examples/next-steps/README.md new file mode 100644 index 0000000..7409350 --- /dev/null +++ b/examples/next-steps/README.md @@ -0,0 +1,13 @@ +this is text + +```shell +echo main +``` + +# Next steps + +The list inside this block are steps that could be followed when performing an interactive tutorial + + 1. [Step #1](step-1.md) + 1. [Step #2](step-2.md) + diff --git a/examples/next-steps/expected_result.out b/examples/next-steps/expected_result.out new file mode 100644 index 0000000..f538837 --- /dev/null +++ b/examples/next-steps/expected_result.out @@ -0,0 +1,5 @@ +main + +Which step do you want to take next? +1.) Step #1 +2.) Step #2 \ No newline at end of file diff --git a/examples/next-steps/expected_result.seo b/examples/next-steps/expected_result.seo new file mode 100644 index 0000000..29ec0e6 --- /dev/null +++ b/examples/next-steps/expected_result.seo @@ -0,0 +1,33 @@ +{ + "body": [ + { + "content": "this is text\n", + "type": "text" + }, + { + "content": [ + "echo main" + ], + "type": "commands" + }, + { + "content": "Next steps", + "level": 1, + "type": "heading" + }, + { + "content": "The list inside this block are steps that could be followed when performing an interactive tutorial\n", + "type": "text" + } + ], + "next_steps": [ + { + "target": "step-1.md", + "title": "Step #1" + }, + { + "target": "step-2.md", + "title": "Step #2" + } + ] +} diff --git a/examples/next-steps/step-1.md b/examples/next-steps/step-1.md new file mode 100644 index 0000000..b07c629 --- /dev/null +++ b/examples/next-steps/step-1.md @@ -0,0 +1,4 @@ + +```shell +echo step-1 +``` \ No newline at end of file diff --git a/examples/next-steps/step-2.md b/examples/next-steps/step-2.md new file mode 100644 index 0000000..9e5fc4d --- /dev/null +++ b/examples/next-steps/step-2.md @@ -0,0 +1,4 @@ + +```shell +echo step-2 +``` \ No newline at end of file diff --git a/examples/prerequisites/README.md b/examples/prerequisites/README.md new file mode 100644 index 0000000..74f3028 --- /dev/null +++ b/examples/prerequisites/README.md @@ -0,0 +1,18 @@ +# Prerequisites + +This is the prerequisite section. SimDem looks for a set of links to extract and run through first + +* [prereq-ignored](./prereq-ignored.md) + +They don't even need to be in the same list + +* [prereq-processed](./prereq-processed.md) + +By this point, the prerequisites have either run or have passed their validation + +# Validation + +```shell +echo prereq_ignored = $prereq_ignored +echo prereq_processed = $prereq_processed +``` diff --git a/examples/prerequisites/expected_result.demo b/examples/prerequisites/expected_result.demo new file mode 100644 index 0000000..b279e81 --- /dev/null +++ b/examples/prerequisites/expected_result.demo @@ -0,0 +1,12 @@ +$ echo prereq_validation_pass +prereq_validation_pass +$ echo prereq_validation_fail +prereq_validation_fail +***PREREQUISITE VALIDATION FAILED*** +$ echo YOU SHOULD SEE THIS +YOU SHOULD SEE THIS +$ prereq_processed=true +$ echo prereq_ignored = $prereq_ignored +prereq_ignored = +$ echo prereq_processed = $prereq_processed +prereq_processed = true diff --git a/examples/prerequisites/expected_result.dump b/examples/prerequisites/expected_result.dump new file mode 100644 index 0000000..6b92bbe --- /dev/null +++ b/examples/prerequisites/expected_result.dump @@ -0,0 +1,17 @@ +{'body': [{'content': 'Prerequisites', 'level': 1, 'type': 'heading'}, + {'content': 'This is the prerequisite section. SimDem looks for a ' + 'set of links to extract and run through first', + 'type': 'text'}, + {'content': "They don't even need to be in the same list", + 'type': 'text'}, + {'content': 'By this point, the prerequisites have either run or ' + 'have passed their validation', + 'type': 'text'}, + {'content': 'Validation', + 'level': 1, + 'type': 'heading'}, + {'content': ['echo prereq_ignored = $prereq_ignored', + 'echo prereq_processed = $prereq_processed'], + 'type': 'commands'}], + 'prerequisites': ['content/simdem1/prereq-ignored.md', + 'content/simdem1/prereq-processed.md']} diff --git a/examples/prerequisites/expected_result.seo b/examples/prerequisites/expected_result.seo new file mode 100644 index 0000000..6c90d1c --- /dev/null +++ b/examples/prerequisites/expected_result.seo @@ -0,0 +1,37 @@ +{ + "body": [ + { + "content": "Prerequisites", + "level": 1, + "type": "heading" + }, + { + "content": "This is the prerequisite section. SimDem looks for a set of links to extract and run through first\n", + "type": "text" + }, + { + "content": "They don't even need to be in the same list\n", + "type": "text" + }, + { + "content": "By this point, the prerequisites have either run or have passed their validation\n", + "type": "text" + }, + { + "content": "Validation", + "level": 1, + "type": "heading" + }, + { + "content": [ + "echo prereq_ignored = $prereq_ignored", + "echo prereq_processed = $prereq_processed" + ], + "type": "commands" + } + ], + "prerequisites": [ + "./prereq-ignored.md", + "./prereq-processed.md" + ] +} diff --git a/examples/prerequisites/expected_result.tutorial b/examples/prerequisites/expected_result.tutorial new file mode 100644 index 0000000..c84a938 --- /dev/null +++ b/examples/prerequisites/expected_result.tutorial @@ -0,0 +1,36 @@ +$ echo prereq_validation_pass +prereq_validation_pass +$ echo prereq_validation_fail +prereq_validation_fail +***PREREQUISITE VALIDATION FAILED*** +# WE ARE IN prereq-processed.md + +This file is designed to have the validation fail. This means that we should completely execute this file + +# Validation + +This is a validation section. If this validation section passes, we stop processing this file + +# Main area + +This should be run since the validation for the prereq has failed + +$ echo YOU SHOULD SEE THIS +YOU SHOULD SEE THIS +# Set a variable that passes through + +$ prereq_processed=true +# Prerequisites + +This is the prerequisite section. SimDem looks for a set of links to extract and run through first + +They don't even need to be in the same list + +By this point, the prerequisites have either run or have passed their validation + +# Validation + +$ echo prereq_ignored = $prereq_ignored +prereq_ignored = +$ echo prereq_processed = $prereq_processed +prereq_processed = true diff --git a/examples/prerequisites/prereq-ignored.md b/examples/prerequisites/prereq-ignored.md new file mode 100644 index 0000000..0566eab --- /dev/null +++ b/examples/prerequisites/prereq-ignored.md @@ -0,0 +1,31 @@ +# WE ARE IN prereq-ignored.md + +This file is designed to have the validation pass. This means that we should stop executing this document after the results section + +# Validation + +This is a validation section. If this validation section passes, we stop processing this file. The command below should be the last text displayed + +``` +echo prereq_validation_pass +``` + +Results: + +``` +prereq_validation_pass +``` + +# Main area + +This should never be run since the validation for the prereq has been met + +``` +echo YOU SHOULD NOT SEE THIS +``` + +# Set a variable that passes through + +``` +prereq_ignored=true +``` diff --git a/examples/prerequisites/prereq-processed.md b/examples/prerequisites/prereq-processed.md new file mode 100644 index 0000000..019053e --- /dev/null +++ b/examples/prerequisites/prereq-processed.md @@ -0,0 +1,31 @@ +# WE ARE IN prereq-processed.md + +This file is designed to have the validation fail. This means that we should completely execute this file + +# Validation + +This is a validation section. If this validation section passes, we stop processing this file + +``` +echo prereq_validation_fail +``` + +Results: + +``` +blah +``` + +# Main area + +This should be run since the validation for the prereq has failed + +``` +echo YOU SHOULD SEE THIS +``` + +# Set a variable that passes through + +``` +prereq_processed=true +``` diff --git a/examples/results-block-fail/README.md b/examples/results-block-fail/README.md new file mode 100644 index 0000000..70f1b4f --- /dev/null +++ b/examples/results-block-fail/README.md @@ -0,0 +1,20 @@ +this is text + +```shell +echo foo +echo bar +``` + +Results: + +```result +barrrrr +``` + +In demo mode, we will continue processing. + +```shell +echo post_result_block_fail +``` + +even more text diff --git a/examples/results-block-fail/expected_result.demo b/examples/results-block-fail/expected_result.demo new file mode 100644 index 0000000..44b24a7 --- /dev/null +++ b/examples/results-block-fail/expected_result.demo @@ -0,0 +1,6 @@ +$ echo foo +foo +$ echo bar +bar +$ echo post_result_block_fail +post_result_block_fail diff --git a/examples/results-block-fail/expected_result.seo b/examples/results-block-fail/expected_result.seo new file mode 100644 index 0000000..e53e5f7 --- /dev/null +++ b/examples/results-block-fail/expected_result.seo @@ -0,0 +1,30 @@ +{ + "body": [ + { + "content": "this is text\n", + "type": "text" + }, + { + "content": [ + "echo foo", + "echo bar" + ], + "expected_result": "barrrrr\n", + "type": "commands" + }, + { + "content": "In demo mode, we will continue processing.\n", + "type": "text" + }, + { + "content": [ + "echo post_result_block_fail" + ], + "type": "commands" + }, + { + "content": "even more text\n", + "type": "text" + } + ] +} diff --git a/examples/results-block-fail/expected_result.tutorial b/examples/results-block-fail/expected_result.tutorial new file mode 100644 index 0000000..94b21dc --- /dev/null +++ b/examples/results-block-fail/expected_result.tutorial @@ -0,0 +1,13 @@ +this is text + +$ echo foo +foo +$ echo bar +bar +*** SIMDEM TEST RESULT FAILED *** +In demo mode, we will continue processing. + +$ echo post_result_block_fail +post_result_block_fail +even more text + diff --git a/examples/results-block/README.md b/examples/results-block/README.md new file mode 100644 index 0000000..f41762e --- /dev/null +++ b/examples/results-block/README.md @@ -0,0 +1,14 @@ +this is text + +```shell +echo foo +echo bar +``` + +Results: + +```result +bar +``` + +even more text diff --git a/examples/results-block/expected_result.demo b/examples/results-block/expected_result.demo new file mode 100644 index 0000000..ac40788 --- /dev/null +++ b/examples/results-block/expected_result.demo @@ -0,0 +1,4 @@ +$ echo foo +foo +$ echo bar +bar diff --git a/examples/results-block/expected_result.seo b/examples/results-block/expected_result.seo new file mode 100644 index 0000000..5076507 --- /dev/null +++ b/examples/results-block/expected_result.seo @@ -0,0 +1,20 @@ +{ + "body": [ + { + "content": "this is text\n", + "type": "text" + }, + { + "content": [ + "echo foo", + "echo bar" + ], + "expected_result": "bar\n", + "type": "commands" + }, + { + "content": "even more text\n", + "type": "text" + } + ] +} diff --git a/examples/results-block/expected_result.tutorial b/examples/results-block/expected_result.tutorial new file mode 100644 index 0000000..2b468c7 --- /dev/null +++ b/examples/results-block/expected_result.tutorial @@ -0,0 +1,9 @@ +this is text + +$ echo foo +foo +$ echo bar +bar +*** SIMDEM TEST RESULT PASSED *** +even more text + diff --git a/examples/setup-script/README.md b/examples/setup-script/README.md new file mode 100644 index 0000000..6a3dacf --- /dev/null +++ b/examples/setup-script/README.md @@ -0,0 +1,13 @@ +# Setup script + +To execute: `simdem -s examples/setup-script/setup.sh examples/setup-script/README.md` + +```shell +echo $FOO +``` + +Results: + +```shell +bar +``` diff --git a/examples/setup-script/setup.sh b/examples/setup-script/setup.sh new file mode 100644 index 0000000..1566bb1 --- /dev/null +++ b/examples/setup-script/setup.sh @@ -0,0 +1 @@ +FOO=bar \ No newline at end of file diff --git a/examples/simdem1/README.md b/examples/simdem1/README.md new file mode 100644 index 0000000..99cadd7 --- /dev/null +++ b/examples/simdem1/README.md @@ -0,0 +1,54 @@ +# Prerequisites + +This is the prerequisite section. SimDem extracts links to run through prior to executing the main steps. + +Here is an example of a prerequisite that will be ignored because its conditions are already met. + +* [prereq-ignored](./prereq-ignored.md) + +Here is an example of a prerequisites that will be run because its conditions are not met. + +* [prereq-processed](./prereq-processed.md) + +By this point, the prerequisites have either run or have passed their validation. + +# Did our prerequisites run? + +```shell +echo prereq_ignored = $prereq_ignored +echo prereq_processed = $prereq_processed +``` + +# Do stuff here + +We want to execute this because the code type is shell. + +```shell +echo foo +var=bar +``` + + +# Do more stuff here + +We assume the result is for the last command of the last code block. + +```shell +echo baz +echo $var +``` + +Results: + +```result +bar +``` + +# Next Steps + +The list inside this block are steps that could be followed when performing an interactive tutorial + + 1. [Step #1](step-1.md) + 1. [Step #2](step-2.md) + + diff --git a/examples/simdem1/expected_result.seo b/examples/simdem1/expected_result.seo new file mode 100644 index 0000000..02815a3 --- /dev/null +++ b/examples/simdem1/expected_result.seo @@ -0,0 +1,89 @@ +{ + "body": [ + { + "content": "Prerequisites", + "level": 1, + "type": "heading" + }, + { + "content": "This is the prerequisite section. SimDem looks for a set of links to extract and run through first\n", + "type": "text" + }, + { + "content": "They don't even need to be in the same list\n", + "type": "text" + }, + { + "content": "By this point, the prerequisites have either run or have passed their validation\n", + "type": "text" + }, + { + "content": "Did our prerequisites run?", + "level": 1, + "type": "heading" + }, + { + "content": [ + "echo prereq_ignored = $prereq_ignored", + "echo prereq_processed = $prereq_processed" + ], + "type": "commands" + }, + { + "content": "Do stuff here", + "level": 1, + "type": "heading" + }, + { + "content": "We want to execute this because the code type is shell\n", + "type": "text" + }, + { + "content": [ + "echo foo", + "var=bar" + ], + "type": "commands" + }, + { + "content": "Do more stuff here", + "level": 1, + "type": "heading" + }, + { + "content": "We assume the result is for the last command of the last code block\n", + "type": "text" + }, + { + "content": [ + "echo baz", + "echo $var" + ], + "expected_result": "bar\n", + "type": "commands" + }, + { + "content": "Next Steps", + "level": 1, + "type": "heading" + }, + { + "content": "The list inside this block are steps that could be followed when performing an interactive tutorial\n", + "type": "text" + } + ], + "next_steps": [ + { + "target": "step-1.md", + "title": "Step #1" + }, + { + "target": "step-2.md", + "title": "Step #2" + } + ], + "prerequisites": [ + "./prereq-ignored.md", + "./prereq-processed.md" + ] +} diff --git a/examples/simdem1/prereq-ignored.md b/examples/simdem1/prereq-ignored.md new file mode 100644 index 0000000..0566eab --- /dev/null +++ b/examples/simdem1/prereq-ignored.md @@ -0,0 +1,31 @@ +# WE ARE IN prereq-ignored.md + +This file is designed to have the validation pass. This means that we should stop executing this document after the results section + +# Validation + +This is a validation section. If this validation section passes, we stop processing this file. The command below should be the last text displayed + +``` +echo prereq_validation_pass +``` + +Results: + +``` +prereq_validation_pass +``` + +# Main area + +This should never be run since the validation for the prereq has been met + +``` +echo YOU SHOULD NOT SEE THIS +``` + +# Set a variable that passes through + +``` +prereq_ignored=true +``` diff --git a/examples/simdem1/prereq-processed.md b/examples/simdem1/prereq-processed.md new file mode 100644 index 0000000..019053e --- /dev/null +++ b/examples/simdem1/prereq-processed.md @@ -0,0 +1,31 @@ +# WE ARE IN prereq-processed.md + +This file is designed to have the validation fail. This means that we should completely execute this file + +# Validation + +This is a validation section. If this validation section passes, we stop processing this file + +``` +echo prereq_validation_fail +``` + +Results: + +``` +blah +``` + +# Main area + +This should be run since the validation for the prereq has failed + +``` +echo YOU SHOULD SEE THIS +``` + +# Set a variable that passes through + +``` +prereq_processed=true +``` diff --git a/examples/simdem1/step-1.md b/examples/simdem1/step-1.md new file mode 100644 index 0000000..b07c629 --- /dev/null +++ b/examples/simdem1/step-1.md @@ -0,0 +1,4 @@ + +```shell +echo step-1 +``` \ No newline at end of file diff --git a/examples/simdem1/step-2.md b/examples/simdem1/step-2.md new file mode 100644 index 0000000..9e5fc4d --- /dev/null +++ b/examples/simdem1/step-2.md @@ -0,0 +1,4 @@ + +```shell +echo step-2 +``` \ No newline at end of file diff --git a/examples/simple-variable/README.md b/examples/simple-variable/README.md new file mode 100644 index 0000000..d420346 --- /dev/null +++ b/examples/simple-variable/README.md @@ -0,0 +1,11 @@ +# this is text + +```shell +FOO=bar +``` + +```shell +echo $FOO +``` + +more text diff --git a/examples/simple-variable/expected_result.demo b/examples/simple-variable/expected_result.demo new file mode 100644 index 0000000..2b21e48 --- /dev/null +++ b/examples/simple-variable/expected_result.demo @@ -0,0 +1,3 @@ +$ FOO=bar +$ echo $FOO +bar diff --git a/examples/simple-variable/expected_result.seo b/examples/simple-variable/expected_result.seo new file mode 100644 index 0000000..b11a28d --- /dev/null +++ b/examples/simple-variable/expected_result.seo @@ -0,0 +1,25 @@ +{ + "body": [ + { + "content": "this is text", + "level": 1, + "type": "heading" + }, + { + "content": [ + "FOO=bar" + ], + "type": "commands" + }, + { + "content": [ + "echo $FOO" + ], + "type": "commands" + }, + { + "content": "more text\n", + "type": "text" + } + ] +} diff --git a/examples/simple-variable/expected_result.tutorial b/examples/simple-variable/expected_result.tutorial new file mode 100644 index 0000000..f603dad --- /dev/null +++ b/examples/simple-variable/expected_result.tutorial @@ -0,0 +1,7 @@ +# this is text + +$ FOO=bar +$ echo $FOO +bar +more text + diff --git a/examples/simple/README.md b/examples/simple/README.md new file mode 100644 index 0000000..d7e193c --- /dev/null +++ b/examples/simple/README.md @@ -0,0 +1,6 @@ +# Simple + +```shell +echo foo +echo bar +``` diff --git a/examples/simple/expected_result.demo b/examples/simple/expected_result.demo new file mode 100644 index 0000000..ac40788 --- /dev/null +++ b/examples/simple/expected_result.demo @@ -0,0 +1,4 @@ +$ echo foo +foo +$ echo bar +bar diff --git a/examples/simple/expected_result.seo b/examples/simple/expected_result.seo new file mode 100644 index 0000000..a7bd765 --- /dev/null +++ b/examples/simple/expected_result.seo @@ -0,0 +1,16 @@ +{ + "body": [ + { + "content": "Simple", + "level": 1, + "type": "heading" + }, + { + "content": [ + "echo foo", + "echo bar" + ], + "type": "commands" + } + ] +} diff --git a/examples/simple/expected_result.tutorial b/examples/simple/expected_result.tutorial new file mode 100644 index 0000000..f47410d --- /dev/null +++ b/examples/simple/expected_result.tutorial @@ -0,0 +1,6 @@ +# Simple + +$ echo foo +foo +$ echo bar +bar diff --git a/examples/toc/README.md b/examples/toc/README.md new file mode 100644 index 0000000..c164c3d --- /dev/null +++ b/examples/toc/README.md @@ -0,0 +1,11 @@ +# This the table of contents test + +## ToC + +* [Main](README.md) +* [One](one.md) +* [Two](two.md) + +## Main content + +Foo \ No newline at end of file diff --git a/examples/toc/expected_result.demo b/examples/toc/expected_result.demo new file mode 100644 index 0000000..99d9dd2 --- /dev/null +++ b/examples/toc/expected_result.demo @@ -0,0 +1,6 @@ + +Next steps available: +1. Main (README.md) +2. One (one.md) +3. Two (two.md) + diff --git a/examples/toc/expected_result.seo b/examples/toc/expected_result.seo new file mode 100644 index 0000000..b5cd60c --- /dev/null +++ b/examples/toc/expected_result.seo @@ -0,0 +1,32 @@ +{ + "body": [ + { + "content": "This the table of contents test", + "level": 1, + "type": "heading" + }, + { + "content": "Main content", + "level": 2, + "type": "heading" + }, + { + "content": "Foo", + "type": "text" + } + ], + "toc": [ + { + "target": "README.md", + "title": "Main" + }, + { + "target": "one.md", + "title": "One" + }, + { + "target": "two.md", + "title": "Two" + } + ] +} diff --git a/examples/toc/expected_result.test b/examples/toc/expected_result.test new file mode 100644 index 0000000..e69de29 diff --git a/examples/toc/expected_result.tutorial b/examples/toc/expected_result.tutorial new file mode 100644 index 0000000..e02b67c --- /dev/null +++ b/examples/toc/expected_result.tutorial @@ -0,0 +1,11 @@ +# This the table of contents test + +## Main content + +Foo + +Next steps available: +1. Main (README.md) +2. One (one.md) +3. Two (two.md) + diff --git a/examples/toc/one.md b/examples/toc/one.md new file mode 100644 index 0000000..55f8b72 --- /dev/null +++ b/examples/toc/one.md @@ -0,0 +1 @@ +# one \ No newline at end of file diff --git a/examples/toc/two.md b/examples/toc/two.md new file mode 100644 index 0000000..fe9a18f --- /dev/null +++ b/examples/toc/two.md @@ -0,0 +1 @@ +# two \ No newline at end of file diff --git a/js/common.js b/js/common.js deleted file mode 100644 index 99e2bcd..0000000 --- a/js/common.js +++ /dev/null @@ -1,19 +0,0 @@ -function sleep(delay) { - var start = new Date().getTime(); - while (new Date().getTime() < start + delay); -} - -function log(type, msg) { - $('#log').prepend('
' + $('
').text(new Date() + " : " + type + " : " + msg).html()); -} - -function open_tab(url) { - var win = window.open(url, '_blank'); - if (win) { - //Browser has allowed it to be opened - win.focus(); - } else { - //Browser has blocked it - alert('Please allow popups for this website'); - } -} diff --git a/js/console.js b/js/console.js deleted file mode 100644 index 3b965c3..0000000 --- a/js/console.js +++ /dev/null @@ -1,22 +0,0 @@ -function init_console() { - var socket = io.connect('http://' + document.domain + ':' + location.port + '/console'); - - socket.on('update_console', function(msg) { - $('#console').append(msg); - $('#console').animate({ - scrollTop: $('#console')[0].scrollHeight}, 500) - log("CONSOLE", msg); - }); - - socket.on('clear', function(msg) { - $('#console').html(''); - log("CONSOLE", "clear") - }); - - socket.on('open_tab', function(url) { - open_tab(url) - log("CONSOLE", "Open tab for " + url) - }); -} - - diff --git a/js/control.js b/js/control.js deleted file mode 100644 index dc96e9f..0000000 --- a/js/control.js +++ /dev/null @@ -1,70 +0,0 @@ -function init_control() { - var socket = io('http://' + document.domain + ':' + location.port + '/control'); - var command_key = "" - var keypress_interval; - - $("#btn_launch_console_window").click(function() { - window.open('/console', '_blank', 'toolbar=0,location=0,menubar=0'); - }) - - socket.on('update_info', function(msg) { - $('#info').append(msg); - $('#info').animate({ - scrollTop: $('#info')[0].scrollHeight}, 500) - log("INFO", msg) - }); - - socket.on('get_command_key', function(msg) { - input = $('').text("Press a command key (h for help)"); - $('#info').append(input); - - $(document).keypress(function(event) { - command_key = String.fromCharCode(event.which) - window.clearInterval(keypress_interval); - $(document).off("keypress") - $('#input').remove() - log("GET_COMMAND_KEY", "Got '" + command_key + "'") - socket.emit('command_key', command_key) - }) - - keypress_interval = window.setInterval(function () { - log("GET_COMAND_KEY", "Waiting for input") - }, 1000); - }); - - socket.on('input_string', function(msg) { - in_str = "" - $('#input_string_wrap').remove() - in_element = $(''); - div = $('
').append(in_element); - $('#info').append(div) - - in_element.on('change', function() { - in_str = $('#input_string').val() - log("GET_INPUT", "Got '" + in_str + "'") - socket.emit('input_string', in_str) - }); - - in_element.keypress(function(event) { - if (event.which == 13) { - in_element.change() - } - }) - - in_element.blur(function(){ - $('#input_string_wrap').remove() - }); - - in_element.focus(); - }); - - socket.on('clear', function(msg) { - $('#info').html(''); - log("INFO", "clear") - }); - - socket.on('log', function(msg) { - log("LOG", msg) - }); -} - diff --git a/main.py b/main.py deleted file mode 100644 index 2d6c2a4..0000000 --- a/main.py +++ /dev/null @@ -1,145 +0,0 @@ -#!/usr/bin/env python3 - -# This is a python script that emulates a terminal session and runs -# commands from a supplied markdown file.. - -import optparse -import os -import sys -import time - -from cli import Ui -from web import WebUi -import config -from demo import Demo -from environment import Environment - -def get_bash_script(script_dir, is_simulation = True, is_automated=False, is_testing=False): - """ - Reads a README.md file in the indicated directoy and builds an - executable bash script from the commands contained within. - """ - if not script_dir.endswith('/'): - script_dir = script_dir + "/" - - script = "" - env = Environment(script_dir, False).get() - for key, value in env.items(): - script += key + "='" + value + "'\n" - - filename = env.get_script_file_name(script_dir) - in_code_block = False - in_results_section = False - lines = list(open(script_dir + filename)) - for line in lines: - if line.startswith("Results:"): - # Entering results section - in_results_section = True - elif line.startswith("```") and not in_code_block: - # Entering a code block, if in_results_section = True then it's a results block - in_code_block = True - elif line.startswith("```") and in_code_block: - # Finishing code block - in_results_section = False - in_code_block = False - elif in_code_block and not in_results_section: - # Executable line - script += line - elif line.startswith("#") and not in_code_block and not in_results_section and not is_automated: - # Heading in descriptive text - script += "\n" - return script - -def main(): - """SimDem CLI interpreter""" - - commands = config.modes - command_string = "" - for command in commands: - command_string = command_string + command + "|" - command_string = command_string[0:len(command_string)-1] - - p = optparse.OptionParser("%prog [" + command_string + "] DEMO_NAME", version=config.SIMDEM_VERSION) - p.add_option('--style', '-s', default="tutorial", - help="The style of simulation you want to run. 'tutorial' (the default) will print out all text and pause for user input before running commands. 'simulate' will not print out the text but will still pause for input.") - p.add_option('--path', '-p', default="demo_scripts/", - help="The Path to the demo scripts directory.") - p.add_option('--auto', '-a', default="False", - help="Set to 'true' (or 'yes') to prevent the application waiting for user keypresses between commands. Set to 'no' when running in test mode to allow users to step through each test.") - p.add_option('--test', '-t', default="False", - help="If set to anything other than False the output of the command will be compared to the expected results in the sript. Any failures will be reported") - p.add_option('--fastfail', default="True", - help="If set to anything other than True test execution has will stop on the first failure. This has no affect if running in any mode other than 'test'.") - p.add_option('--debug', '-d', default="False", - help="Turn on debug logging by setting to True.") - p.add_option('--webui', '-w', default="False", - help="If set to anything other than False will interact with the user through a Web UI rather than the CLI.") - p.add_option('--output', '-o', default="log", - help="Format of the output. The default is `log` which will output all stdout data. Other options are `summary` which provides a summary of the execution status and `json`") - - options, arguments = p.parse_args() - - if not options.path.endswith("/"): - options.path += "/" - - if options.auto == "False": - is_automatic = False - else: - is_automatic = True - - if options.test == "False": - is_test = False - else: - is_test = True - - if options.fastfail == "True": - is_fast_fail= True - else: - is_fast_fail= False - - if options.style == "simulate": - simulate = True - elif options.style == 'tutorial': - simulate = False - else: - print("Unknown style (--style, -s): " + options.style) - exit(1) - - if options.debug.lower() == "true": - config.is_debug = True - - if len(arguments) == 2: - script_dir = options.path + arguments[1] - else: - script_dir = options.path - - cmd = None - if len(arguments) > 0: - cmd = arguments[0] - # 'run' is deprecated in the CLI, but not yet removed from code - if cmd == "tutorial": - cmd = "run" - if cmd == "test": - is_test = True - is_auto = True - - filename = "README.md" - is_docker = os.path.isfile('/.dockerenv') - demo = Demo(is_docker, script_dir, filename, simulate, is_automatic, is_test, is_fast_fail, output_format=options.output); - - if options.webui == "False": - ui = Ui() - else: - ui = WebUi(config.port) - print("Server started. Listening on port " + str(ui.port)) - print("Point your browser at " + str(ui.port)) - print() - while not ui.ready: - time.sleep(0.25) - print("Waiting for client connection") - cmd = None - - demo.set_ui(ui) - demo.run(cmd) - -main() diff --git a/novnc/.XDefaults b/novnc/.XDefaults deleted file mode 100644 index 1c21c22..0000000 --- a/novnc/.XDefaults +++ /dev/null @@ -1,15 +0,0 @@ -XTerm.VT100.background: Black -XTerm.VT100.color0: Black -XTerm.VT100.color1: Red -XTerm.VT100.color2: Green -XTerm.VT100.color3: Yellow -XTerm.VT100.color4: CornflowerBlue -XTerm.VT100.color5: Magenta -XTerm.VT100.color6: Cyan -XTerm.VT100.color7: White -XTerm.VT100.colorBD: White -XTerm.VT100.colorBDMode: true -XTerm.VT100.colorUL: Yellow -XTerm.VT100.colorULMode: true -XTerm.VT100.cursorColor: Red -XTerm.VT100.foreground: White diff --git a/novnc/.config/autostart/xterm.desktop b/novnc/.config/autostart/xterm.desktop deleted file mode 100644 index 11cf322..0000000 --- a/novnc/.config/autostart/xterm.desktop +++ /dev/null @@ -1,10 +0,0 @@ -[Desktop Entry] -Version=1.0 -Type=Application -Name=Terminal -Comment=Use the command line -Exec=xterm -Icon=xterm-color -Path= -Terminal=false -StartupNotify=false diff --git a/novnc/Desktop/Terminal Emulator.desktop b/novnc/Desktop/Terminal Emulator.desktop deleted file mode 100755 index 11cf322..0000000 --- a/novnc/Desktop/Terminal Emulator.desktop +++ /dev/null @@ -1,10 +0,0 @@ -[Desktop Entry] -Version=1.0 -Type=Application -Name=Terminal -Comment=Use the command line -Exec=xterm -Icon=xterm-color -Path= -Terminal=false -StartupNotify=false diff --git a/pre-commit.sh b/pre-commit.sh deleted file mode 100755 index 542a69f..0000000 --- a/pre-commit.sh +++ /dev/null @@ -1,33 +0,0 @@ -#! /bin/sh -# script to run tests on what is to be committed -# Thanks Torek, -# https://stackoverflow.com/questions/20479794/how-do-i-properly-git-stash-pop-in-pre-commit-hooks-to-get-a-clean-working-tree - -# First, stash index and work dir, keeping only the -# to-be-committed changes in the working directory. -echo "Stash changes not ready to be committed" -old_stash=$(git rev-parse -q --verify refs/stash) -git stash save -q --keep-index -new_stash=$(git rev-parse -q --verify refs/stash) - -# If there were no changes (e.g., `--amend` or `--allow-empty`) -# then nothing was stashed, and we should skip everything, -# including the tests themselves. (Presumably the tests passed -# on the previous commit, so there is no need to re-run them.) -if [ "$old_stash" = "$new_stash" ]; then - echo "pre-commit script: no changes to test" - sleep 1 # XXX hack, editor may erase message - exit 0 -fi - -echo "Run the Simdem tests" -python3 main.py -p demo_scripts/test test -RESULT=$? - -echo "Restore unstaged changes" -git reset --hard -q && git stash apply --index -q && git stash drop -q - -# Exit with status from test-run: nonzero prevents commit -exit $RESULT - - diff --git a/release_process.md b/release_process.md deleted file mode 100644 index 036d8d1..0000000 --- a/release_process.md +++ /dev/null @@ -1,67 +0,0 @@ -# Releasig Simdem - -This document describes the process for releasing SimDem. - -# Ensure we are building master - -``` -git checkout master -git pull upstream master -git push -``` - -# Test - -``` -./script/install.sh -simdem -p demo_scripts/test test -``` - -# Remove '-dev' from the version number - -The '-dev' prefix is used to indicate unreleased version, therefore it -should be removed. - -In `config.py` change the line that starts with `SIMDEM_VERSION = `. - -# Docker Containers - -## Build containers - -``` -./scripts/build.sh -``` - -## Publish the containers - -``` -./scripts/publish.sh -``` - -# Tag Git - -```` -git tag -a $SIMDEM_VERSION -m "Build and publish $SIMDEM_VERSION" -git push origin 0.8.1 -git push upstream 0.8.1 -``` - -# Bump the version number - -Increment the version number as appropriate, adding '-dev' to indicate -this is an unreleased version. In `config.py` change the line that -starts with `SIMDEM_VERSION = `. - -## Update version number in tests - -In `demo_sctipts/test/README.md` update the expected version number in -the first test.` - -# Commit the new version number - -``` -git add config.py -git add demo_scripts/test/README.md -git commit -m "bump version number" -git push -``` diff --git a/requirements.txt b/requirements.txt index c464dc7..150fe1a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,4 @@ -colorama -flask -flask-socketio -pexpect - +nose==1.3.7 +mistletoe==0.5.3 +pexpect==4.2.1 +ddt==1.1.1 diff --git a/scent.py b/scent.py new file mode 100644 index 0000000..e6afa02 --- /dev/null +++ b/scent.py @@ -0,0 +1,34 @@ +from sniffer.api import * # import the really small API +import os, termstyle + +# you can customize the pass/fail colors like this +pass_fg_color = termstyle.green +pass_bg_color = termstyle.bg_default +fail_fg_color = termstyle.red +fail_bg_color = termstyle.bg_default + +# All lists in this variable will be under surveillance for changes. +watch_paths = ['.'] + +# this gets invoked on every file that gets changed in the directory. Return +# True to invoke any runnable functions, False otherwise. +# +# This fires runnables only if files ending with .py extension and not prefixed +# with a period. +@file_validator +def py_files(filename): + return (filename.endswith('.py') or filename.endswith('.md')) and not os.path.basename(filename).startswith('.') + +# This gets invoked for verification. This is ideal for running tests of some sort. +# For anything you want to get constantly reloaded, do an import in the function. +# +# sys.argv[0] and any arguments passed via -x prefix will be sent to this function as +# it's arguments. The function should return logically True if the validation passed +# and logicially False if it fails. +# +# This example simply runs nose. +@runnable +def execute_nose(*args): + import nose + return nose.run(argv=list(args)) + diff --git a/scripts/build.sh b/scripts/build.sh deleted file mode 100755 index 3c8c417..0000000 --- a/scripts/build.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/bin/bash - -# Builds a SimDem container. -# -# Usage: -# -# build.sh - builds both containers (cli and novnc) -# build.sh novnc - builds the novnc version of the container -# build.sh cli - builds the CLI version of the container - -REPOSITORY=rgardler -FLAVOR=${1:-} -IMAGE_NAME_PREFIX=simdem_ - -VERSION=`grep SIMDEM_VERSION config.py | awk '{print $3}' | tr -d '"'` - -build_container() { - docker build -f Dockerfile_$1 -t $REPOSITORY/${IMAGE_NAME_PREFIX}$1:$VERSION . - - if [ $? -eq 0 ]; then - echo "Built $REPOSITORY/${IMAGE_NAME_PREFIX}$1:$VERSION" - else - echo "Failed to build $REPOSITORY/${IMAGE_NAME_PREFIX}$1:$VERSION" - return 0 - fi -} - -if [[ $FLAVOR == "novnc" ]]; then - build_container novnc -elif [[ $FLAVOR == "cli" ]]; then - build_container cli -else - build_container cli - if [ $? -ne 0 ]; then - echo "Building container failed. Exiting" - return 1 - fi - build_container novnc -fi - -exit $? diff --git a/scripts/install.sh b/scripts/install.sh deleted file mode 100755 index 8e00993..0000000 --- a/scripts/install.sh +++ /dev/null @@ -1,86 +0,0 @@ -function removeEarlier() { -# Remove < 0.7.0 version - if [ -f ~/bin/simdem.py ]; then - echo "Since version 0.7.0 Simdem is installed in a different way, we need to remove the old Simdem version." - rm ~/bin/simdem.py - rm ~/bin/simdem - fi - - if [ -f /usr/local/bin/simdem.py ]; then - echo "Since version 0.7.0 Simdem is installed in a different way, we need to remove the old Simdem version." - sudo rm /usr/local/bin/simdem.py - sudo rm /usr/local/bin/simdem - fi - - if [ -f /.dockerenv ]; then - echo "Running in a Docker container" - IS_DOCKER=true - INSTALL_DIR=~/bin/simdem-dev/ - else - echo "Not running in a Docker container" - IS_DOCKER=false - INSTALL_DIR=/usr/local/bin/simdem-dev/ - fi -} - -function installLinuxDependencies() { - if [ "$IS_DOCKER" = true ]; then - apt update - apt-get install -y python3-pip - else - sudo apt update - sudo apt-get install -y python3-pip - fi -} - -if [ -f /.dockerenv ]; then - echo "Running in a Docker container" - IS_DOCKER=true - BIN_DIR=~/bin -else - echo "Not running in a Docker container" - IS_DOCKER=false - BIN_DIR=/usr/local/bin -fi - -INSTALL_DIR=$BIN_DIR/simdem-dev/ -MAIN_FILE=main.py -SYMLINK=simdem - -unameOut="$(uname -s)" -case "${unameOut}" in - Linux*) installLinuxDependencies;; - Darwin*) brew install python3;; - *) echo "Unsupported OS: ${unameOut}" -esac - -virtualenv simdem-env -source simdem-env/bin/activate - -pip3 install -r requirements.txt - -if [ "$IS_DOCKER" = true ]; then - mkdir -p $INSTALL_DIR - - cp -r * $INSTALL_DIR - chmod +x $INSTALL_DIR$MAIN_FILE - - echo 'export PATH=$PATH:'$BIN_DIR >> ~/.bashrc - - if [ ! -L $INSTALL_DIR../$SYMLINK ]; then - ln -s $INSTALL_DIR$MAIN_FILE $INSTALL_DIR../$SYMLINK - fi -else - echo "Make install directory at $INSTALL_DIR" - sudo mkdir -p $INSTALL_DIR - - echo "Copy source into install dir" - sudo cp -r * $INSTALL_DIR - echo "Make main file executable" - sudo chmod +x $INSTALL_DIR$MAIN_FILE - - if [ ! -L $INSTALL_DIR../$SYMLINK ]; then - echo "Create symlink to main.py file" - sudo ln -s $INSTALL_DIR$MAIN_FILE $INSTALL_DIR../$SYMLINK - fi -fi diff --git a/scripts/publish.sh b/scripts/publish.sh deleted file mode 100755 index f26d7f3..0000000 --- a/scripts/publish.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/bin/bash - -# Publishes SimDem containers. -# -# Usage: -# -# publish.sh - publiches both containers (cli and novnc) -# publish.sh novnc - publishes the novnc version of the container -# publish.sh cli - publishes the CLI version of the container - -REPOSITORY=rgardler -FLAVOR=${1:-} -CONTAINERNAME=simdem_$FLAVOR - -VERSION=`grep -Po '(?<=SIMDEM_VERSION = \")(.*)(?=\")' config.py` - -print_result() { - if [ $1 -eq 0 ]; then - echo "Published $2" - else - echo "Failed to publish $2" - exit 1 - fi -} - - -if [[ $FLAVOR == "novnc" ]]; then - docker push $REPOSITORY/$CONTAINERNAME:$VERSION - print_result $? "$REPOSITORY/$CONTAINERNAME:$VERSION" -elif [[ $FLAVOR == "cli" ]]; then - docker push $REPOSITORY/$CONTAINERNAME:$VERSION - print_result $? "$REPOSITORY/$CONTAINERNAME:$VERSION" - -else - docker push $REPOSITORY/${CONTAINERNAME}cli:$VERSION - print_result $? "$REPOSITORY/${CONTAINERNAME}cli:$VERSION" - - docker push $REPOSITORY/${CONTAINERNAME}novnc:$VERSION - print_result $? "$REPOSITORY/${CONTAINERNAME}novnc:$VERSION" -fi diff --git a/scripts/run.sh b/scripts/run.sh deleted file mode 100755 index 9e6886c..0000000 --- a/scripts/run.sh +++ /dev/null @@ -1,96 +0,0 @@ -#!/bin/bash - -# Runs either a command line or headless VNC Docker container with -# Simdem installed. -# -# Usage: run.sh [FLAVOR] [SCRIPT_DIR] [MODE] -# -# FLAVOR is an optional parameter to define which container to run, -# either the `novnc` or the `cli` versions. If not specified the -# `novnc` version is used -# -# SCRIPT_DIR is an optional parameter to define a directory containing -# SimDem scripts that will be mounted into the final container. Default -# value is `./demo_scripts`. -# -# MODE is an optional parameter that is only used by the CLI -# container, the novnc container will ignore it. MODE defines the mode -# of execution. It can take the values 'tutorial' (default, run with -# full instructional text), 'demo' (run with no instructional text and -# simulate typed commnds', 'test' (run in auto mode and test results -# against expected results) -# -# Connect with a browser at http://YOUR_DOCKER_HOST:8080/?password=vncpassword -# -# Based on https://github.com/ConSol/docker-headless-vnc-container - - -# Configuration options: -# You can configure the following variables to change behaviour of -# the container. -# -# VNC config -# ========== -VNC_COL_DEPTH='24' -VNC_RESOLUTION='1024x768' -VNC_PW='vncpassword' - -NO_VNC_PORT=8080 - -FLAVOR=${1:-novnc} -SCRIPTS_DIR=${2:-`pwd`/demo_scripts} -MODE=${3:-tutorial} -REPOSITORY=rgardler -CONTAINER_NAME=simdem_$FLAVOR -SCRIPTS_VOLUME=${CONTAINER_NAME}_scripts -AZURE_VOLUME=${AZURE_VOLUME:-$HOME/.azure} -SSH_VOLUME=${SSH_VOLUME:-$HOME/.ssh} - -if [[ $FLAVOR == "novnc" ]]; then - HOME="/headless" -else - HOME="/home/simdem" -fi - -VERSION=`grep SIMDEM_VERSION config.py | awk '{print $3}' | tr -d '"'` - -echo Running $REPOSITORY/$CONTAINER_NAME:$VERSION - -echo Stopping and removing pre-existing containers -docker stop $CONTAINER_NAME -docker rm $CONTAINER_NAME -docker stop $SCRIPTS_VOLUME -docker rm $SCRIPTS_VOLUME - -echo Creating scripts data container named $SCRIPTS_VOLUME containing the scripts in $SCRIPTS_DIR -docker create -v $HOME/demo_scripts --name $SCRIPTS_VOLUME ubuntu /bin/true -docker cp $SCRIPTS_DIR/. $SCRIPTS_VOLUME:$HOME/demo_scripts/ - -echo starting the $CONTAINER_NAME container in mode $MODE - -if [[ $MODE == "tutorial" ]]; then - COMMAND="tutorial" -elif [[ $MODE == "demo" ]]; then - COMMAND="run --style simulate" -elif [[ $MODE == "test" ]]; then - COMMAND="test" -fi - -if [[ $FLAVOR == "novnc" ]]; then - docker run -d -p 5901:5901 -p 8080:$NO_VNC_PORT --name $CONTAINER_NAME \ - --volume $AZURE_VOLUME:$HOME/.azure \ - --volume $SSH_VOLUME:$HOME/.ssh \ - --volumes-from $SCRIPTS_VOLUME \ - -e VNC_COL_DEPTH=$VNC_COL_DEPTH \ - -e VNC_RESOLUTION=$VNC_RESOLUTION \ - -e VNC_PW=$VNC_PW \ - $REPOSITORY/$CONTAINER_NAME:$VERSION -else - docker run -it \ - --volume $AZURE_VOLUME:$HOME/.azure \ - --volume $SSH_VOLUME:$HOME/.ssh \ - --volumes-from $SCRIPTS_VOLUME \ - --name $CONTAINER_NAME \ - $REPOSITORY/$CONTAINER_NAME:$VERSION \ - $COMMAND -fi diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..cf14acf --- /dev/null +++ b/setup.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +""" Setup.py """ + +# Created from https://github.com/kennethreitz/setup.py + +from setuptools import setup, find_packages + + +with open('README.md') as f: + README = f.read() + +with open('LICENSE') as f: + LICENSE = f.read() + +setup( + name='simdem', + version='0.9.0', + description='SimDem', + long_description=README, + author='Tommy Falgout, Ross Gardler', + author_email='thfalgou@microsoft.com', + url='https://github.com/Azure/simdem', + license='MIT', + packages=find_packages(exclude=('tests', 'docs')), + entry_points={ + 'console_scripts': [ + 'simdem=simdem.cli:main' + ] + } +) diff --git a/simdem/__init__.py b/simdem/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/simdem/cli.py b/simdem/cli.py new file mode 100755 index 0000000..93805c4 --- /dev/null +++ b/simdem/cli.py @@ -0,0 +1,143 @@ +#!/usr/local/bin/python3 +""" Entrypoint to Simdem """ +import configparser +import logging +import argparse +import os +import pkg_resources + +from simdem.executor import bash +from simdem.parser import ast, simdem1 +from simdem.mode import demo, dump, test, tutorial, cleanup +from simdem.ui import basic, color + +def main(): + """ Main execution function """ + argp = argparse.ArgumentParser(prog='simdem') + argp.add_argument('file', metavar='file', + help='file to process') + argp.add_argument('--debug', '-d', action="store_true", + help="Turn on logging to console") + argp.add_argument('--config-file', '-c', + help="Config file to use") + argp.add_argument('--mode', '-m', default="tutorial", + help="Mode to use", choices=['demo', 'dump', 'test', 'tutorial', 'cleanup']) + argp.add_argument('--parser', '-p', default="simdem1", + help="Parser class to use", choices=['simdem1', 'ast']) + argp.add_argument('--shell', '-s', default="bash", + help="Shell class to use", choices=['bash']) + argp.add_argument('--environment', '-e', default='', + help="Environment variables to inject (Example: -e FOO=foo1,BAR=bar1)") + argp.add_argument('--boot-strap', '-b', default=None, + help="Boot strap script to execute") + argp.add_argument('--ui', '-u', default='color', + help="UI class to use", choices=['basic', 'color']) + argp.add_argument('--override-config', metavar='override', + help="Override setting in config file") + version = pkg_resources.require("simdem")[0].version + argp.add_argument('--version', action='version', version='%(prog)s ' + version, + help="Display SimDem's version number") + options = argp.parse_args() + + file_path = options.file + config_file_path = get_config_file_path(options) + validate(file_path, config_file_path) + + config = configparser.ConfigParser() + config.read(config_file_path) + inject_config_options(options, config) + + setup_logging(config, options) + + mode = get_mode(options, config) + + # Add user's home directory to path + mode.process_command("export HOME=" + os.path.expanduser("~")) + + if options.environment: + commands = options.environment.split(',') + mode.process_commands(commands) + + if options.boot_strap: + mode.run_setup_script(options.setup_script) + + mode.process_file(file_path) + +def get_config_file_path(options): + """ Returns the found config file path """ + options_config_file = options.config_file + if options_config_file: + return options_config_file + file_path = pkg_resources.resource_filename(__name__, 'simdem.ini') + return file_path + +def inject_config_options(options, config): + """ Injects CLI arguments into config settings """ + if options.override_config: + [key, value] = options.override_config.split('=') + [section, option] = key.split('.') + config.set(section, option, value) + +def validate(file_path, config_file_path): + """ validate all passed in arguments """ + if not os.path.isfile(file_path): + raise FileNotFoundError('Unable to find file: ' + file_path) + + if not os.path.isfile(config_file_path): + raise FileNotFoundError('Unable to find config file: ' + config_file_path) + +def get_mode(options, config): + """ Returns correct renderer object """ + + parser = get_parser(options) + shell = get_shell(options) + ui = get_ui(options, config) + + if options.mode == 'demo': + return demo.DemoMode(config, parser, shell, ui) + + if options.mode == 'dump': + return dump.DumpMode(config, parser, shell, ui) + + if options.mode == 'cleanup': + return cleanup.CleanupMode(config, parser, shell, ui) + + if options.mode == 'test': + return test.TestMode(config, parser, shell, ui) + + if options.mode == 'tutorial': + return tutorial.TutorialMode(config, parser, shell, ui) + +def get_ui(options, config): + """ return UI object """ + if options.ui == 'basic': + return basic.BasicUI(config) + elif options.ui == 'color': + return color.ColorUI(config) + +def get_parser(options): + """ Returns correct parser object """ + if options.parser == 'ast': + return ast.AstParser() + elif options.parser == 'simdem1': + return simdem1.SimDem1Parser() + +def get_shell(options): + """ Returns correct shell object """ + if options.shell == 'bash': + return bash.BashExecutor() + +def setup_logging(config, options): + """ Establishes logging level and format """ + log_formatter = logging.Formatter(config.get('log', 'format', raw=True)) + root_logger = logging.getLogger() + root_logger.setLevel(config.get('log', 'level')) + + file_handler = logging.FileHandler(config.get('log', 'file')) + file_handler.setFormatter(log_formatter) + root_logger.addHandler(file_handler) + + if options.debug: + console_handler = logging.StreamHandler() + console_handler.setFormatter(log_formatter) + root_logger.addHandler(console_handler) diff --git a/simdem/executor/__init__.py b/simdem/executor/__init__.py new file mode 100644 index 0000000..767b2f0 --- /dev/null +++ b/simdem/executor/__init__.py @@ -0,0 +1,2 @@ +""" Import each of the executors """ +from .bash import BashExecutor diff --git a/simdem/executor/bash.py b/simdem/executor/bash.py new file mode 100644 index 0000000..10bd9bc --- /dev/null +++ b/simdem/executor/bash.py @@ -0,0 +1,56 @@ +""" This file contains the BashExecutor object """ + +import logging + +import pexpect +from pexpect import replwrap + +PEXPECT_PROMPT = u'[PEXPECT_PROMPT>' +PEXPECT_CONTINUATION_PROMPT = u'[PEXPECT_PROMPT+' + +class BashExecutor(object): + """ This class is used to execute Bash commands + Required functions: run_cmd() + """ + + _shell = None + _env = {'PS1': '"$ "'} + + def __init__(self): + pass + + + def run_cmd(self, command): + """ Runs the command passed in the shell + """ + + command = command.strip() + logging.debug("Execute command: '" + command + "'") + if not command: + logging.debug('Empty command. Not executing') + return '' + response = self.get_shell().run_command(command) + # https://pexpect.readthedocs.io/en/stable/overview.html#find-the-end-of-line-cr-lf-conventions + # Because pexpect respects TTY (which uses CRLF) instead of UNIX, we must swap out. + # This might get tricky if we start supporting windows + # This is because to easily write expected testcase output files, + # most unix-ish text editors write with \n + response = response.replace("\r\n", "\n") + logging.debug("Response: '" + response + "'") + return response + + def get_shell(self): + """Gets or creates the shell in which to run commands for the + supplied demo + """ + + if self._shell is None: + # Should we use spawn or spawnu? + # The prompts used to be u'\[\]', but pylint prefers r'\[\]'. + # Noting just in case this bites us later + child = pexpect.spawnu('/bin/bash', env=self._env, echo=False, timeout=None) + ps1 = PEXPECT_PROMPT[:5] + r'\[\]' + PEXPECT_PROMPT[5:] + ps2 = PEXPECT_CONTINUATION_PROMPT[:5] + r'\[\]' + PEXPECT_CONTINUATION_PROMPT[5:] + prompt_change = u"PS1='{0}' PS2='{1}' PROMPT_COMMAND=''".format(ps1, ps2) + self._shell = replwrap.REPLWrapper(child, r'\$', prompt_change) + return self._shell diff --git a/simdem/misc/getch.py b/simdem/misc/getch.py new file mode 100644 index 0000000..86f9f48 --- /dev/null +++ b/simdem/misc/getch.py @@ -0,0 +1,40 @@ +# pylint: disable=R0903,E0401,W0612 +""" Get Character Class """ + +# https://stackoverflow.com/a/510404/8475874 +class Getch: + """Gets a single character from standard input. Does not echo to the screen.""" + def __init__(self): + try: + self.impl = GetchWindows() + except ImportError: + self.impl = GetchUnix() + + def __call__(self): + return self.impl() + + +class GetchUnix: + """ Get character impl for *nix """ + def __call__(self): + import sys + import tty + import termios + filedesc = sys.stdin.fileno() + old_settings = termios.tcgetattr(filedesc) + try: + tty.setraw(sys.stdin.fileno()) + char = sys.stdin.read(1) + finally: + termios.tcsetattr(filedesc, termios.TCSADRAIN, old_settings) + return char + + +class GetchWindows: + """ Get character impl for *nix """ + def __init__(self): + import msvcrt + + def __call__(self): + import msvcrt + return msvcrt.getch() diff --git a/simdem/mode/__init__.py b/simdem/mode/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/simdem/mode/cleanup.py b/simdem/mode/cleanup.py new file mode 100644 index 0000000..ba67feb --- /dev/null +++ b/simdem/mode/cleanup.py @@ -0,0 +1,19 @@ +""" Automated mode for SimDem """ + +import logging +from simdem.mode.common import ModeCommon + +class CleanupMode(ModeCommon): + """ This class is the SimDem Cleanup mode + """ + + def process(self, steps): + """ I'd like to use a dispatcher for this; however, we need to exit processing + if the validation fails. """ + logging.debug("process()") + if 'cleanup' in steps: + self.process_commands(steps['cleanup']['commands']) + + def process_next_steps(self, steps, start_path): + """ No need to display next steps if in test mode """ + pass diff --git a/simdem/mode/common.py b/simdem/mode/common.py new file mode 100644 index 0000000..7ea36f9 --- /dev/null +++ b/simdem/mode/common.py @@ -0,0 +1,151 @@ +""" Common mode for SimDem mode """ + +import os +import logging +import difflib +import pathlib + +class ModeCommon(object): # pylint: disable=R0903 + """ This class is designed to hold any shared code across modes + """ + config = None + executor = None + parser = None + render = None + + def __init__(self, config, parser, executor, ui): + self.config = config + self.parser = parser + self.executor = executor + self.ui = ui + + def run_setup_script(self, file_path): + """ Runs setup script """ + logging.debug("run_setup_script(" + file_path + ")") + + cmd = '. ' + file_path + self.ui.print_prompt() + self.print_command(cmd) + self.ui.print_break() + result = self.executor.run_cmd(cmd) + self.ui.print_result(result) + self.ui.print_break() + + def process_file(self, file_path, is_prereq=False, toc={}): + """ Parses the file and starts processing it """ + logging.debug("parse_file(file_path=" + file_path + ", is_prereq=" + str(is_prereq)) + # Change the working directory in case of any recursion + start_path = os.path.dirname(os.path.abspath(file_path)) + + self.setup_temp_dir() + logging.debug('parse_file::start_path=' + start_path) + steps = self.parser.parse_file(file_path, is_prereq) + + # https://github.com/Azure/simdem/issues/92 + env_file = start_path + '/./env.sh' + if os.path.isfile(env_file): + self.process_env_file(env_file) + + # We want to inherit the parent's TOC to reduce the # of copies needed + if toc: + logging.debug('Adding parent POC') + steps['toc'] = toc + logging.debug(steps) + + # Begin preqreq processing + if 'prerequisites' in steps: + for prereq_file in steps['prerequisites']: + # Change the working directory in case of any recursion + self.process_file(start_path + '/' + prereq_file, is_prereq=True) + if is_prereq and 'validation' in steps: + last_command_result = self.process_commands(steps['validation']['commands']) # pylint: disable=no-member + if 'expected_result' in steps['validation']: + if self.is_result_valid(steps['validation']['expected_result'], + last_command_result): + return + else: + self.ui.print_validation_failed() + # End prereq processing + + if start_path: + self.executor.run_cmd('cd ' + start_path) + self.process(steps) # pylint: disable=no-member + + if 'toc' in steps: + self.process_next_steps(steps['toc'], start_path) # pylint: disable=no-member + + def process_env_file(self, env_file): + """ Run the env file. Assumes it exists """ + logging.debug('process_env_file(' + env_file + ')') + env_fh = open(env_file) + env_contents = env_fh.readlines() + env_fh.close() + self.process_commands(env_contents) + + def process_commands(self, cmds, display=True): + """ Pretend to type the command, run it and then display the output """ + for cmd in cmds: + result = self.process_command(cmd, display=display) + if display: + self.ui.print_break() + return result + + def process_command(self, cmd, display=True): + """ Process single command """ + if display: + self.ui.print_prompt() + self.print_command(cmd) + self.ui.print_break() + result = self.executor.run_cmd(cmd) + if display: + self.ui.print_result(result) + return result + + def print_command(self, cmd): + """ Default action to print the command is to just call the UI. """ + self.ui.print_cmd(cmd) + + @staticmethod + def is_result_valid(expected_results, actual_results, expected_similarity=0.8): + """Checks to see if a command execution passes. + If actual results compared to expected results is within + the expected similarity level then it's considered a pass. + + expected_similarity = 1.0 could be a breaking change for older SimDem scripts. + explicit fails > implicit passes + Ross may disagree with me. Let's see how this story unfolds. + """ + + if not actual_results: + logging.error("is_result_valid(): actual_results is empty.") + return False + + logging.debug("is_result_valid(" + expected_results + "," + actual_results + \ + "," + str(expected_similarity) + ")") + + expected_results_str = expected_results.rstrip() + actual_results_str = actual_results.rstrip() + logging.debug("is_result_valid(" + expected_results_str + "," + actual_results_str + \ + "," + str(expected_similarity) + ")") + seq = difflib.SequenceMatcher(lambda x: x in " \t\n\r", + actual_results_str, + expected_results_str) + + is_pass = seq.ratio() >= expected_similarity + + if is_pass: + logging.info("is_result_valid passed") + + else: + logging.error("is_result_valid failed") + logging.error("actual_results = " + actual_results) + logging.error("expected_results = " + expected_results) + + return is_pass + + def setup_temp_dir(self): + """ https://github.com/Azure/simdem/issues/104 """ + directory = str(pathlib.Path.home()) + '/' + self.config.get('main', 'temp_dir', raw=True) + logging.info("temp_dir=" + directory) + self.process_command("mkdir -p " + directory, display=False) + self.process_command("export SIMDEM_TEMP_DIR=" + directory, display=False) \ No newline at end of file diff --git a/simdem/mode/demo.py b/simdem/mode/demo.py new file mode 100644 index 0000000..796da5d --- /dev/null +++ b/simdem/mode/demo.py @@ -0,0 +1,32 @@ +"""Demo (default) mode for SimDem""" + +import random +import time +import logging +from simdem.mode.interactive import InteractiveMode + +class DemoMode(InteractiveMode): + """ This class is the default SimDem file processor. + It's designed for running files in a demo-able mode that looks like a human is typing it + """ + + def process(self, steps): + """ Processes the steps from a processed file """ + logging.debug("process()") + + for step in steps['body']: + if step['type'] == 'commands': + self.process_commands(step['content']) + + def print_command(self, cmd): + """ Displays the command on the screen """ + + # Must add ' ' when typing command because whitespaces are removed from configparser + # https://docs.python.org/3/library/configparser.html#supported-ini-file-structure + for _, char in enumerate(cmd): + if char != "\n": + typing_delay = float(self.config.get('render', 'typing_delay')) + if typing_delay: + delay = random.uniform(0.02, typing_delay) + time.sleep(delay) + self.ui.print(char) diff --git a/simdem/mode/dump.py b/simdem/mode/dump.py new file mode 100644 index 0000000..ef87640 --- /dev/null +++ b/simdem/mode/dump.py @@ -0,0 +1,26 @@ +""" Debug renderer for SimDem""" + +import logging +import json +import configparser +from simdem.mode.common import ModeCommon + +class DumpMode(ModeCommon): # pylint: disable=R0903 + """ This class is used to pretty print a parsed file """ + + def process_file(self, file_path, is_prereq=False): + """ Parse the file and print it. Not very exciting. """ + logging.debug("parse_file(file_path=" + file_path + ", is_prereq=" + str(is_prereq)) + steps = self.parser.parse_file(file_path) + self.ui.println(json.dumps(steps, indent=4, sort_keys=True)) + + def print_config_data(self): + """ Dead code for now, but useful for debugging config """ + self.ui.println("Config=") + for section in self.config.sections(): + for option in self.config.options(section): + try: + self.ui.println(section + '.' + option + '=' + str(self.config.get(section, option))) + except configparser.InterpolationMissingOptionError: + pass + self.ui.println() diff --git a/simdem/mode/interactive.py b/simdem/mode/interactive.py new file mode 100644 index 0000000..31a48f1 --- /dev/null +++ b/simdem/mode/interactive.py @@ -0,0 +1,107 @@ +""" Interactive Mode Class """ +import sys +import logging +from collections import deque +from simdem.mode.common import ModeCommon + +class InteractiveMode(ModeCommon): + """ Interactive Mode subclass """ + + def process_commands(self, cmds, last_text=None, display=True): + """ Loop through the commands to run as well as expect interrupt logic from the user """ + result = None + cmd = None + cmd_deque = deque(cmds) + # https://twitter.com/sandwich_cool/status/956932558847176704 + # The "I smell danger" picture is never truer than now + # This statement is written in VSCode while I work for MSFT + while True: + self.ui.print_prompt() + key = self.ui.get_single_key_input() + result = self.process_command_input(key, last_command=cmd, last_text=last_text) + logging.debug(cmd_deque) + + if result: + # We received a special key. Loop through again + continue + elif cmd_deque: + # Are there still commands left to be run? + cmd = cmd_deque.popleft() + result = self.run_command(cmd, display=display) + if not cmd_deque: + # We've completed all commands + break + return result + + def run_command(self, cmd, display=True): + """ Pretend to type the command, run it and then display the output """ + # Request enter from user to know when to proceed + logging.debug('run_command(' + cmd + ')') + + if display: + self.print_command(cmd) + # For some reason this requires a print_break() while common does not. Too late to debug + self.ui.print_break() + result = self.executor.run_cmd(cmd) + if display: + self.ui.print_result(result) + return result + + def process_command_input(self, key, last_command=None, last_text=None): + """ Process the command input. It's 4AM and I'm sleepy + For now, just return. We'll implement that later """ + result = None + if key == 'b': + logging.debug('Received Break request') + self.ui.print("\nshell> ") + command = self.ui.get_line_input('') + if command != "": + result = self.executor.run_cmd(command) + self.ui.print_result(result) + elif key == 'r': + logging.debug('Received Run last command request') + if last_command: + result = self.run_command(last_command) + elif key == 'd': + logging.debug('Received Show last description request') + if last_text: + self.ui.println(last_text) + result = last_text + logging.debug('Output=' + str(result)) + return result + # Otherwise, we will return and assume the user wants to continue + + def process_next_steps(self, next_steps, start_path): + """ Is there a good way to test this that doesn't involve lots of test code + expect? + Not fully tested yet. Low priority feature. + """ + idx = 1 + if next_steps: + self.ui.println() + self.ui.println("Next steps available:") + for step in next_steps: + self.ui.println(str(idx) + ". " + step['title'] + " (" + step['target'] + ") ") + idx += 1 + self.ui.println() + # https://stackoverflow.com/questions/1077113/how-do-i-detect-whether-sys-stdout-is-attached-to-terminal-or-not + if sys.stdout.isatty(): + # You're running in a real terminal + in_string = "" + in_value = 0 + + while in_value < 1 or in_value > len(next_steps): + prompt = "Choose a step. Enter a value between 1 and " + \ + str(len(next_steps)) + " or 'q' to quit: " + in_string = self.ui.get_line_input(prompt) + if in_string.lower() == "quit" or in_string.lower() == "q": + return + try: + in_value = int(in_string) + except ValueError: + pass + + self.process_file(start_path + '/' + next_steps[int(in_string) - 1]['target'], toc=next_steps) + else: + logging.info('Not connected to a TTY terminal Not requesting input.') + # You're being piped or redirected + return diff --git a/simdem/mode/test.py b/simdem/mode/test.py new file mode 100644 index 0000000..ca6a6c6 --- /dev/null +++ b/simdem/mode/test.py @@ -0,0 +1,29 @@ +""" Automated mode for SimDem """ + +import logging +from simdem.mode.common import ModeCommon + +class TestMode(ModeCommon): + """ This class is the automated SimDem mode + Does not display the descriptive text, but pauses at each + code block. When the user hits a key the command is "typed", a + second keypress executes the command. + """ + + def process(self, steps): + """ I'd like to use a dispatcher for this; however, we need to exit processing + if the validation fails. """ + logging.debug("process()") + for step in steps['body']: + if step['type'] == 'commands': + last_command_result = self.process_commands(step['content']) + if 'expected_result' in step: + if self.is_result_valid(step['expected_result'], last_command_result): + self.ui.print_test_passed() + else: + self.ui.print_test_failed() + exit(1) + + def process_next_steps(self, steps, start_path): + """ No need to display next steps if in test mode """ + pass diff --git a/simdem/mode/tutorial.py b/simdem/mode/tutorial.py new file mode 100644 index 0000000..78c3f82 --- /dev/null +++ b/simdem/mode/tutorial.py @@ -0,0 +1,33 @@ +""" Tutorial mode for SimDem""" + +import os +import sys +import logging +from simdem.mode.interactive import InteractiveMode + +class TutorialMode(InteractiveMode): + """ This class is the tutorial mode class for SimDem. + It's designed for running files in a tutorial mode which Displays the descriptive text + of the tutorial and pauses at code blocks to allow user interaction. + """ + + def process(self, steps): + """ Processes the steps from a processed file """ + logging.debug("process()") + + last_text = None +# self.ui.clear() + for step in steps['body']: + if step['type'] == 'heading': + self.ui.print_heading(step['content'], step['level']) + elif step['type'] == 'text': + self.ui.println(step['content']) + last_text = step['content'] + elif step['type'] == 'commands': + last_command_result = self.process_commands(step['content'], last_text=last_text) + logging.debug(step) + if 'expected_result' in step: + if self.is_result_valid(step['expected_result'], last_command_result): + self.ui.print_test_passed() + else: + self.ui.print_test_failed() diff --git a/simdem/parser/__init__.py b/simdem/parser/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/simdem/parser/ast.py b/simdem/parser/ast.py new file mode 100644 index 0000000..4f7f530 --- /dev/null +++ b/simdem/parser/ast.py @@ -0,0 +1,22 @@ +""" This module hosts the ContextParser +""" + +import mistletoe.ast_renderer as renderer +import mistletoe.block_token + + +class AstParser(object): # pylint: disable=R0903 + """ This class parses the human readable markdown using a defined syntax to + know how to create the SimDem Execution Object + and uses the code block language to know which type of execution it is + """ + + @staticmethod + def parse_file(file_path): + """ The main meat for parsing the file. Uses mistletoe's AST parser + to create a tokenized object and then parses that tokenized object + into SimDem Execution Object + """ + with open(file_path, 'r') as fin: + ast = renderer.get_ast(mistletoe.block_token.Document(fin)) + return ast diff --git a/simdem/parser/simdem1.py b/simdem/parser/simdem1.py new file mode 100644 index 0000000..7277095 --- /dev/null +++ b/simdem/parser/simdem1.py @@ -0,0 +1,208 @@ +""" This module hosts the ContextParser +""" + +import logging +from collections import defaultdict +from mistletoe.base_renderer import BaseRenderer +from mistletoe import Document + + +class SimDem1Parser(object): # pylint: disable=R0903 + """ Parses markdown based off of Mistletoe renderer """ + + @staticmethod + def parse_file(file_path, is_prerequisite=False): + """ The main meat for parsing the file. """ + with open(file_path, 'r') as fin: + with SimDemMistletoeRenderer(is_prerequisite) as renderer: + rendered = renderer.render(Document(fin)) + + # Do stuff here + return rendered + + +class SimDemMistletoeRenderer(BaseRenderer): + """ Based off of https://gist.github.com/miyuchina/a06bd90d91b70be0906266760547da62 + """ + section = None + block = None + _is_prerequisite = False + + def __init__(self, is_prerequisite): + super().__init__() + self.output = defaultdict(list) + self.reset_section() + self._is_prerequisite = is_prerequisite + + def render_heading(self, token): + """ Render for Heading: # """ + inner = self.render_inner(token) + + # Prerequisite Heading + if inner.lower() == 'prerequisites': + self.set_section('prerequisites') + + # ToC (legacy: Next Steps) Heading + elif inner.lower() == 'next steps' or inner.lower() == 'toc': + self.set_section('toc') + return inner + + # Validation Heading + elif self._is_prerequisite and inner.lower() == 'validation': + self.set_section('validation') + + # Cleanup + elif inner.lower() == 'cleanup': + self.set_section('cleanup') + return inner + + else: + logging.debug("parse_file():unable to determing header type.") + self.reset_section() + + content = {'type': 'heading', 'level': token.level, 'content': inner} + self.append_body(content) + return inner + + def render_list(self, token): + """ Render a markdown list """ + inner = self.render_inner(token) + if self.section is None: + self.output['body'].append({'type': 'text', 'content': ' '}) + return inner + + def render_list_item(self, token): + """ Render a markdown list item """ + inner = self.render_inner(token) + if self.section is None: + self.output['body'].append({'type': 'text', 'content': ' * ' + inner}) + return inner + + def render_link(self, token): + """ Due to the way the parser works, we can only return strings + We need to find another way to store link targets. + Unfortunately, I can only think of storing them in an array right now + """ + inner = self.render_inner(token) + if self.section and 'prerequisites' in self.section: + self.append_prereq(token.target) + if self.section and 'toc' in self.section: + self.append_toc(inner, token.target) + return inner + + def render_raw_text(self, token): + """ Render raw text. The only thing to look for is the result text indicator """ + if token.content.rstrip() == 'Results:': + self.set_block('results') + return '' + return token.content + + def render_emphasis(self, token): + """ Render for inline code """ + inner = self.render_inner(token) + if inner: + body = {'type': 'text', 'content': '*' + str(inner) + '*'} + self.append_body(body) + return '' + + def render_inline_code(self, token): + """ Render for inline code """ + return '`' + self.render_inner(token) + '`' + + def render_paragraph(self, token): + """ Render for Paragraph """ + inner = self.render_inner(token) + if inner: + body = {'type': 'text', 'content': inner} + self.append_body(body) + return '' + + def render_block_code(self, token): + """ Render a markdown block code """ + #lines = token.children[0].content.splitlines() + content = token.children[0].content + + if self.section == 'validation': + # Validation blocks aren't run like normal blocks + if self.block == 'results': + # Validation result blocks are expecially not checked like normal blocks + self.set_validation_result(content) + else: + self.set_validation_command(content) + elif self.section == 'cleanup': + self.set_cleanup(content) + else: + if self.block == 'results': + # Assume that the last body item is the command we're expecting results for + self.output['body'][-1]['expected_result'] = content + else: + # Assume this is a normal code block to run block + content = {'type': 'commands', 'content': content.splitlines()} + self.append_body(content) + + inner = self.render_inner(token) + # After this code block, reset the block type (e.g. No longer a "Result block") + self.set_block(None) + + return inner + + def render_document(self, token): + """ Render the entire markdown document """ + self.render_inner(token) + return dict(self.output) + + def __getattr__(self, name): + """ Kudos to @miyuchina for this suggestion + https://github.com/Azure/simdem/pull/89#issuecomment-370445949 + """ + if name.startswith('render'): + return lambda token: '' + raise AttributeError('{} has no attribute {}'.format(repr(type(self).__name__), repr(name))) + + + # Everything below here is boring setters + + def reset_section(self): + """ If we encounter a new section, reset everything we know """ + self.section = None + self.block = None + + def set_section(self, section): + """ Set the section to the section name. Duh """ + logging.debug('set_section(' + str(section) + ')') + self.section = section + + def set_block(self, block): + """ Set the block to the block name. Duh """ + logging.debug('set_block(' + str(block) + ')') + self.block = block + + def set_validation_command(self, cmds): + """ Assuming validation commands should be a list """ + logging.debug('append_validation(' + str(cmds) + ')') + self.output['validation'] = {'commands': cmds.splitlines()} + + def set_validation_result(self, result): + """ Assuming validation results should be a list """ + logging.debug('append_validation(' + str(result) + ')') + self.output['validation']['expected_result'] = result + + def append_body(self, body): + """ Adding the meat of the work to the dict """ + logging.debug('append_body(' + str(body) + ')') + self.output['body'].append(body) + + def append_prereq(self, target): + """ Set Prereqs """ + logging.debug('append_prereq(' + target + ')') + self.output['prerequisites'].append(target) + + def append_toc(self, name, target): + """ Add steps to table of contents section """ + logging.debug('append_toc(' + name + ',' + target + ')') + self.output['toc'].append({'title': name, 'target': target}) + + def set_cleanup(self, cmds): + """ Assuming cleanup commands are a list """ + logging.debug('set_cleanup_command(' + str(cmds) + ')') + self.output['cleanup'] = {'commands': cmds.splitlines()} diff --git a/simdem/simdem.ini b/simdem/simdem.ini new file mode 100644 index 0000000..e6bfa37 --- /dev/null +++ b/simdem/simdem.ini @@ -0,0 +1,19 @@ +[meta] +simdem_version = 0.9.0 + +[main] +temp_dir = .simdem + +# Logging +[log] +file = simdem.log +level = DEBUG +format = %(asctime)s [%(threadName)-12.12s] [%(levelname)-5.5s] %(message)s + +[render] +# When in demo mode we insert a small random delay between characters. +# TYPING DELAY is the upper bound of this delay. +typing_delay = 0.1 + +# Prompt to use in the console +console_prompt = $ \ No newline at end of file diff --git a/simdem/ui/basic.py b/simdem/ui/basic.py new file mode 100644 index 0000000..25188f7 --- /dev/null +++ b/simdem/ui/basic.py @@ -0,0 +1,12 @@ +""" Basic Render Class """ + +import os +import sys +from simdem.misc.getch import Getch +from simdem.ui.common import CommonUI + +class BasicUI(CommonUI): + """ No frills, no thrills render object """ + + def __init__(self, config): + self.config = config diff --git a/simdem/ui/color.py b/simdem/ui/color.py new file mode 100644 index 0000000..1e7eaa7 --- /dev/null +++ b/simdem/ui/color.py @@ -0,0 +1,64 @@ +""" Basic Render Class """ + +import os +import sys +import colorama +from simdem.misc.getch import Getch +from simdem.ui.common import CommonUI + +colorama.init(strip=None) + +class ColorUI(CommonUI): + """ No frills, no thrills render object """ + + def __init__(self, config): + self.config = config + + def println(self, output='', color=colorama.Fore.WHITE + colorama.Style.BRIGHT): + self.display(output, color=color) + + def print(self, output='', color=None): + self.display(output, end="", flush=True, color=color) + + def print_validation_failed(self): + self.println('***PREREQUISITE VALIDATION FAILED***', color=colorama.Fore.RED + colorama.Style.BRIGHT) + + @staticmethod + def clear(): + # https://www.quora.com/Is-there-a-Clear-screen-function-in-Python + #print("\033[H\033[J") + if sys.stdout.isatty(): + os.system('clear') + + def print_test_passed(self): + self.println('*** SIMDEM TEST RESULT PASSED ***', color=colorama.Fore.GREEN + colorama.Style.BRIGHT) + + def print_test_failed(self): + self.println('*** SIMDEM TEST RESULT FAILED ***', color=colorama.Fore.RED + colorama.Style.BRIGHT) + + def print_heading(self, content, level): + """ Print out the heading exactly as we found it """ + self.println(level * '#' + ' ' + content, color=colorama.Fore.CYAN + colorama.Style.BRIGHT) + self.print_break() + + def print_prompt(self): + self.print(self.config.get('render', 'console_prompt', raw=True) + ' ', color=colorama.Fore.WHITE) + + def print_cmd(self, cmd): + self.print(cmd, color=colorama.Fore.WHITE + colorama.Style.BRIGHT) + + def print_result(self, result): + self.print(result, color=colorama.Fore.GREEN + colorama.Style.BRIGHT) + + def print_break(self): + self.println() + + def display(self, text, color=None, flush=False, end='\n'): + """ Display some text in a given color. Do not print a new line unless + new_line is set to True. + """ + if color: + print(color, end="") + print(text, end=end, flush=flush) + if color: + print(colorama.Style.RESET_ALL, end="") \ No newline at end of file diff --git a/simdem/ui/common.py b/simdem/ui/common.py new file mode 100644 index 0000000..a00d328 --- /dev/null +++ b/simdem/ui/common.py @@ -0,0 +1,74 @@ +""" Basic Render Class """ + +import os +import sys +from simdem.misc.getch import Getch + +class CommonUI(object): + """ No frills, no thrills render object """ + + def __init__(self, config): + self.config = config + + @staticmethod + def get_single_key_input(): + """ SimDem1 uses this method: + https://stackoverflow.com/questions/983354/how-do-i-make-python-to-wait-for-a-pressed-key + For SimDem2, I'm trying this alternative to allow for Windows compatibility + https://stackoverflow.com/questions/510357/python-read-a-single-character-from-the-user/510404#510404 + + https://stackoverflow.com/questions/1077113/how-do-i-detect-whether-sys-stdout-is-attached-to-terminal-or-not + Might need to allow config override of the conditional by allowing a config variable. + Experienced an issue where it hung running in a container and I didn't completely debug + """ + + if sys.stdout.isatty(): + getch = Getch() + return getch.impl() + return + + @staticmethod + def get_line_input(prompt): + """ Request single line from user """ + return input(prompt) + + @staticmethod + def println(output=''): + print(output, flush=True) + + @staticmethod + def print(output=''): + print(output, end="", flush=True) + + def print_validation_failed(self): + self.println('***PREREQUISITE VALIDATION FAILED***') + + @staticmethod + def clear(): + # https://www.quora.com/Is-there-a-Clear-screen-function-in-Python + #print("\033[H\033[J") + if sys.stdout.isatty(): + os.system('clear') + + def print_test_passed(self): + self.println('*** SIMDEM TEST RESULT PASSED ***') + + def print_test_failed(self): + self.println('*** SIMDEM TEST RESULT FAILED ***') + + def print_heading(self, content, level): + """ Print out the heading exactly as we found it """ + self.println(level * '#' + ' ' + content) + self.print_break() + + def print_prompt(self): + self.print(self.config.get('render', 'console_prompt', raw=True) + ' ') + + def print_cmd(self, cmd): + self.print(cmd) + + def print_result(self, result): + self.print(result) + + def print_break(self): + self.println() \ No newline at end of file diff --git a/style/common.css b/style/common.css deleted file mode 100644 index c818889..0000000 --- a/style/common.css +++ /dev/null @@ -1,8 +0,0 @@ -#container { - margin: 0 auto; -} - -.twrap { - overflow: hidden; - margin: 4px 4px 4px 4px -} diff --git a/style/console.css b/style/console.css deleted file mode 100644 index d6de5d9..0000000 --- a/style/console.css +++ /dev/null @@ -1,27 +0,0 @@ -.console { - background-color: black; - color: white; - font-family: "Lucida Console", Monaco, monospace; - width: 48%; - height: 80vh; - overflow: auto; - float: right -} - -.full_screen { - width: 100%; - height: 100vh; -} - -span.prompt { - color: White -} - -span.command { - color: white -} - -span.results { - color: LightGreen -} - diff --git a/style/speaker.css b/style/speaker.css deleted file mode 100644 index 918b840..0000000 --- a/style/speaker.css +++ /dev/null @@ -1,47 +0,0 @@ -.info { - background-color: black; - color: white; - font-family: "Lucida Console", Monaco, monospace; - width: 48%; - height: 80vh; - overflow: auto; - float: left; -} - -.debug { - width: 100%; - clear: both -} - -span.console_input { - display: block; - background-color: white; - color: black; - margin: 2px 2px 2px 2px; -} - -span.heading { - color: Cyan; -} - -span.description { - color: Lime; -} - -span.next_step { - color: RebeccaPurple; -} - -span.warning { - color: Red; -} - -span.request_input { - color: Purple -} - -.console_input { - width: 100% -} - - diff --git a/templates/console.html b/templates/console.html deleted file mode 100644 index 225c831..0000000 --- a/templates/console.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - SimDem Console View - - - - - - - - - - - - - - -
-
- {{ console }} -
-
- - diff --git a/templates/index.html b/templates/index.html deleted file mode 100644 index 3ccf1c2..0000000 --- a/templates/index.html +++ /dev/null @@ -1,44 +0,0 @@ - - - - SimDem Control Center - - - - - - - - - - - - - - - -
-
- {{ console }} -
-
-
- -
-

Control Panel

- -
- -
-

Log

-
-
-
-
- - diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_bash.py b/tests/test_bash.py new file mode 100644 index 0000000..0507c71 --- /dev/null +++ b/tests/test_bash.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +""" Advanced test cases for SimDem """ + +import unittest + +from simdem.executor import bash + +class SimDemTestSuite(unittest.TestCase): + """Advanced test cases.""" + + bash = None + + def setUp(self): + self.bash = bash.BashExecutor() + + def test_run_cmd(self): + """ Validate running a command only prints out the result """ + self.assertEqual("foobar\n", self.bash.run_cmd('echo foobar')) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_mistletoe.py b/tests/test_mistletoe.py new file mode 100644 index 0000000..70c607e --- /dev/null +++ b/tests/test_mistletoe.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +"""SimDem Test Case""" + +import unittest + +import mistletoe.ast_renderer as renderer +import mistletoe.block_token as token + + +class SimDemMistletoeTestSuite(unittest.TestCase): + """Lexer test cases.""" + + def test_ast(self): + """Verify we understand how the Mistletoe AST parsers work""" + #self.maxDiff = None + + file_path = 'examples/simple/README.md' + with open(file_path, 'r') as fin: + res = renderer.get_ast(token.Document(fin)) + exp_res = {'children': [{'children': [{'content': 'Simple', 'type': 'RawText'}], + 'level': 1, + 'type': 'Heading'}, + {'children': [{'content': 'echo foo\necho bar\n', + 'type': 'RawText'}], + 'language': 'shell', + 'type': 'CodeFence'}], + 'footnotes': {}, + 'type': 'Document'} + + self.assertEqual(res, exp_res) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_mode_demo.py b/tests/test_mode_demo.py new file mode 100644 index 0000000..1c8b953 --- /dev/null +++ b/tests/test_mode_demo.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +""" system level test class """ + +import configparser +import logging +import sys +import unittest + +from io import StringIO +from ddt import data, ddt + +from simdem.parser import simdem1 +from simdem.executor import bash +from simdem.mode import demo +from simdem.ui import basic + +@ddt +class SimDemSystemTestSuite(unittest.TestCase): + """Advanced test cases.""" + + simdem = None + + def setUp(self): + config = configparser.ConfigParser() + config.read("examples/config/unit_test.ini") + self.demo = demo.DemoMode(config, simdem1.SimDem1Parser(), bash.BashExecutor(), + basic.BasicUI(config)) + + log_formatter = logging.Formatter(config.get('log', 'format', raw=True)) + root_logger = logging.getLogger() + root_logger.setLevel(config.get('log', 'level')) + file_handler = logging.FileHandler(config.get('log', 'file')) + file_handler.setFormatter(log_formatter) + root_logger.addHandler(file_handler) + sys.stdout = StringIO() + + # https://docs.python.org/3/library/unittest.html#unittest.TestResult.buffer + @data('simple', 'simple-variable', 'results-block', 'toc', + 'results-block-fail', 'prerequisites', 'environment-files') + def test_process(self, directory): + """ Each examples directory is expected to have a README.md and an expected_result.demo + this allows us to test each of them easily + """ + self.demo.process_file('./examples/' + directory + '/README.md') + # Unsure why Pylint complains that 'TextIOWrapper' has no 'getvalue' member. + # I'm not Python smart enough yet to know why this works, but Pylint says it shouldn't. + res = sys.stdout.getvalue() + exp_file = open('./examples/' + directory + '/expected_result.tutorial', 'r') + exp_res = exp_file.read() + exp_file.close() + exp_res = open('./examples/' + directory + '/expected_result.demo', 'r').read() + self.assertEqual(exp_res, res) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_mode_tutorial.py b/tests/test_mode_tutorial.py new file mode 100644 index 0000000..e487d77 --- /dev/null +++ b/tests/test_mode_tutorial.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +""" system level test class """ + +import configparser +import logging +import sys +import unittest + +from io import StringIO +from ddt import data, ddt + +from simdem.parser import simdem1 +from simdem.executor import bash +from simdem.mode import tutorial +from simdem.ui import basic + +@ddt +class SimDemSystemTestSuite(unittest.TestCase): + """Advanced test cases.""" + + simdem = None + + def setUp(self): + + config = configparser.ConfigParser() + config.read("examples/config/unit_test.ini") + self.simdem = tutorial.TutorialMode(config, simdem1.SimDem1Parser(), bash.BashExecutor(), + basic.BasicUI(config)) + + log_formatter = logging.Formatter(config.get('log', 'format', raw=True)) + root_logger = logging.getLogger() + root_logger.setLevel(config.get('log', 'level')) + file_handler = logging.FileHandler(config.get('log', 'file')) + file_handler.setFormatter(log_formatter) + root_logger.addHandler(file_handler) + sys.stdout = StringIO() + + # https://docs.python.org/3/library/unittest.html#unittest.TestResult.buffer + @data('simple', 'simple-variable', 'results-block', 'toc', + 'results-block-fail', 'prerequisites', 'environment-files') + def test_process(self, directory): + """ Each examples directory is expected to have a README.md and an expected_result.tutorial + this allows us to test each of them easily + """ + self.maxDiff = None # pylint: disable=C0103 + + self.simdem.process_file('./examples/' + directory + '/README.md') + # Unsure why Pylint complains that 'TextIOWrapper' has no 'getvalue' member. + # I'm not Python smart enough yet to know why this works, but Pylint says it shouldn't. + res = sys.stdout.getvalue() + exp_file = open('./examples/' + directory + '/expected_result.tutorial', 'r') + exp_res = exp_file.read() + exp_file.close() + self.assertEqual(exp_res, res) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_parser_simdem1.py b/tests/test_parser_simdem1.py new file mode 100644 index 0000000..be085b6 --- /dev/null +++ b/tests/test_parser_simdem1.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +""" system level test class """ + +import configparser +import unittest +import json + +from ddt import data, ddt + +from simdem.parser import simdem1 + +@ddt +class SimDem1ParserTestSuite(unittest.TestCase): + """Advanced test cases.""" + + parser = None + + def setUp(self): + config = configparser.ConfigParser() + config.read("examples/config/unit_test.ini") + + self.parser = simdem1.SimDem1Parser() + + + # https://docs.python.org/3/library/unittest.html#unittest.TestResult.buffer + #@data('markdown-syntax') + @data('simple', 'simple-variable', 'results-block', 'toc', + 'results-block-fail', 'prerequisites', 'environment-files') + def test_process(self, directory): + """ Each examples directory is expected to have a README.md and an expected_result.seo + this allows us to test each of them easily + """ + self.maxDiff = None # pylint: disable=C0103 + res = self.parser.parse_file('./examples/' + directory + '/README.md') + + # Research how to read dict from file + exp_res = json.load(open('./examples/' + directory + '/expected_result.seo', 'r')) + self.assertEqual(exp_res, res) + + +if __name__ == '__main__': + unittest.main() diff --git a/web.py b/web.py deleted file mode 100644 index c7f2fd0..0000000 --- a/web.py +++ /dev/null @@ -1,232 +0,0 @@ -from flask import Flask, send_from_directory -from flask import render_template -from flask_socketio import SocketIO -import threading -import time - -from cli import Ui -import config - -ui = None -app = Flask(__name__) -app.config['SECRET_KEY'] = 'secret!' -socketio = SocketIO(app) -thread = None -command_key = None -in_string = None - -def background_thread(): - while True: - socketio.sleep(10) - if ui.ready: - text = "Server connection alive." - - socketio.emit('log', - text, - namespace='/control') - -@socketio.on('connect', namespace='/control') -def connect(): - global thread - global ui - while ui is None: - time.sleep(0.25) - - ui.clear() - - if thread is None: - thread = socketio.start_background_task(target=background_thread) - ui.ready = True - - print("Connection in /control namespace") - -@socketio.on('command_key', namespace='/control') -def got_command_key(key): - global command_key - command_key = key - -@socketio.on('input_string', namespace='/control') -def got_input_String(in_str): - global in_string - in_string = in_str - -@app.route('/js/') -def send_js(filename): - return send_from_directory('js', filename) - -@app.route('/style/') -def send_style(filename): - return send_from_directory('style', filename) - -@app.route('/') -def index(): - return render_template('index.html', console = "Initializing...") - -@app.route('/console') -def console(): - return render_template('console.html', console= "$") - -class WebUi(Ui): - def __init__(self, port=8080): - global ui - import logging - logging.basicConfig(filename='error.log',level=logging.DEBUG) - ui = self - self.port = port - self.ready = False - t = threading.Thread(target=socketio.run, args=(app, '0.0.0.0', port)) - t.start() - - def prompt(self): - """Display the prompt for the user. This is intended to indicate that - the user is expected to take an action at this point. - """ - self._send_to_console(config.console_prompt, "prompt") - - def command(self, text): - """Display a command, or a part of a command tp be executed.""" - self._send_to_console(text, "command", False) - - def results(self, text): - """Display the results of a command execution""" - self._send_to_console(self.demo.strip_ansi(text), "results", True) - - def clear(self): - """Clears the console and info panel ready for a new section of the script.""" - socketio.emit('clear', - namespace='/console') - socketio.emit('clear', - namespace='/control') - self.prompt() - - def heading(self, text): - """Display a heading""" - self._send_to_info(text, "heading", True) - self.new_line() - - def description(self, text): - """Display some descriptive text. Usually this is text from the demo - document itself. - - """ - # fixme: color self.display(text, colorama.Fore.CYAN) - self._send_to_info(text, "description", True) - - def next_step(self, index, title): - """Displays a next step item with an index (the number to be entered -to select it) and a title (to be displayed). - """ - self._send_to_info(str(index) + " " + title, "next_step", True) - - def instruction(self, text): - """Display an instruction for the user. - """ - self._send_to_info(text, "instruction", True) - - def warning(self, text): - """Display a warning to the user. - """ - self._send_to_info(text, "warning", True) - - def new_para(self, div = "console"): - """Starts a new paragraph in the indicated div.""" - self.new_line(div) - self.new_line(div) - - def new_line(self, div = "console"): - """Send a single new line to te indicated div""" - if div == "console": - self._send_to_console("
") - elif div == "info": - self._send_to_info("
") - - def horizontal_rule(self): - self._send_to_info("

============================================

") - - def display(self, text, color, new_line=False): - """Display some text in a given color. Do not print a new line unless - new_line is set to True. - - """ - self._send_to_info(text, color, new_line) - - def request_input(self, text): - """Displays text that is intended to propmt the user for input and - then waits for input.""" - self._send_to_info(text, "request_input", True) - return self.input_string().lower() - - def _send_to_console(self, text, css_class = "description", new_line = False): - """ Send a string to the console. If new_line is set to true then also send a
""" - text = text.replace("\n", "
") - html = "" + text + "" - if new_line: - html += "
" - - socketio.emit('update_console', - html, - namespace='/console') - - def _send_to_info(self, text, css_class = "description", new_line = False): - """ Send a string to the console. If new_line is set to true then also send a
""" - html = "" + text + "" - if new_line: - html += "
" - - socketio.emit('update_info', - html, - namespace='/control') - - def get_instruction_key(self): - """Gets an instruction from the user. See get_help() for details of - relevant keys to respond with. - - """ - global command_key - command_key = None - socketio.emit('get_command_key', - namespace='/control') - while command_key is None: - pass - - return command_key - - def input_string(self): - """ Get a string from the user.""" - global in_string - in_string = None - socketio.emit('input_string', - namespace='/control') - while in_string is None: - pass - return in_string - - def run_special_command(self, command): - """Test to see if the command is a spcial command that needs to be - handled diferently, these include: - - `xdg-open $URL` - intercepted and converted to an instruction to open a new window - - Returns the response from the command if it was handled by this function, - otherwise returns False. - - """ - orig_command = command - if command.startswith("xdg-open "): - self.warning("Since you are running in Web UI mode it is not possible to execute xdg-open commands.") - self.warning("Attempting to open a new browser tab instead.") - self.warning("Note that this may break tests.") - - command = self.expand_vars(command) - url = command[9:] - - socketio.emit('open_tab', - url, - namespace='/console') - - return "" - else: - return False - - -