Workshop: Remote Control Panel

We’re going to build an app to run unit tests from the web, using nothing but Python, with Anvil.

To follow along, you need to be able to access the Anvil Editor. Create a free account using the following link:

Step 1: The Run Button

Open the Anvil Editor to get started.

In the top-left there is a ‘Create New App’ button. Click it and select the Material Design theme.

You are now in the Anvil Editor.

First, name the app. Click on the name at the top of the screen and type in a name like ‘Test Manager’.

Now we’ll build a User Interface. The toolbox on the right contains components that you can drag-and-drop onto the Design view in the centre.

Drop a Label into the blue bar at the top, where it says ‘Drop title here’. In the Properties panel on the right, enter a title into the text section.

Add a Card to the page. Inside the Card, add a Label, a TextBox and a Button.

Set the Label’s text to say Times to run: and set its role to input-prompt.

Set the TextBox’s type to number and its text to 1. Change its name to run_number_box.

Set the Button’s text to Run Tests and its role to primary-color. Change its name to run_tests_button. Align it to the left to make it sit against the TextBox.

Your app should now look something like this:

The next step is to make it do something.

At the bottom of the Properties tool for the Button is a list of events that we can bind methods to. Click the arrow next to the click event:

You should see some code that looks like this:

def run_tests_button_click(self, **event_args):
  """This method is called when the button is clicked"""
  pass

Remove the pass and add a call to the built-in alert function:

  def run_tests_button_click(self, **event_args):
    """This method is called when the button is clicked"""
    alert("Test runs requested: {}".format(self.run_number_box.text, title="Test run")

When you click the Button, you’ll get a dialog box displaying the number of test runs requested.

Step 2: Displaying Test Runs

Let’s make the Button do something a bit more interesting.

Add a Repeating Panel to your app. This is a component that displays the same piece of UI for each element in a list (or other iterable).

Double-click on the Repeating Panel in the Designer. You’ll see most of the page grayed out, and you can drop things into the Repeating Panel’s template. Drop a Card into it, and into that Card, put a Label whose text is set to ‘Date/Time’. Drop another Label next to it and leave it blank for now - it will hold the date and time that the test was run.

Arrange some more Labels until you have a UI that can display the number of tests run, passed, failed and with errors.

To populate the page with a bunch of empty test result cards, just set the Repeating Panel’s items attribute to an arbitrary list:

class Form1(Form1Template):

  def __init__(self, **properties):
    # ...
    self.repeating_panel_1.items = [1, 2, 3, 4]

Run your app to see some empty results cards:

Step 3: Displaying Data

Let’s make the Repeating Panel display some data.

Change the [1, 2, 3, 4] from Step 2 to be an empty list, so the Repeating Panel is empty when the app starts:

self.repeating_panel_1.items = []

And append some fake data to this list when the Button is clicked:

from datetime import datetime
from random import randint
# ...

  def run_tests_button_click(self, **event_args):
    """This method is called when the button is clicked"""
    for i in range(self.run_number_box.text):
      tests_run = 6
      passed = randint(0, tests_run)
      failed = tests_run - passed
      errors = 0

      self.repeating_panel_1.items = [{
        'date_time': datetime.now(),
        'tests_run': tests_run,
        'passed': passed,
        'failed': failed,
        'errors': errors,
      }] + self.repeating_panel_1.items

Run your app and hit the ‘Run Tests’ button. You’ll see a number of test result cards corresponding to the number of test runs the user selected.

Let’s get the data into the result card. For each empty Label, click on it in the Design view and add a Data Binding in the Properties window:

Data Bindings tie the value of a property to the value of a Python expression. In this case we’re tying the text of the Label to the test run data. Since these Labels are within the Repeating Panel, each element of self.repeating_panel_1.items is available to the Label as self.item. So the Data Bindings are:

and similar for the 'passed', 'failed' and 'errors' Labels.

The 'date_time' label needs to format the datetime object into a string using the strftime method:

Clicking on the ‘Run Tests’ button will now generate randomly populated result cards.

Step 4: Get Something To Test

An app like this could manage and report on the stages of a complex build and deployment pipeline. But to keep it simple for this workshop, we’ll clone a very simple Git repo and run the unit tests.

The repo in question is a Roman Numeral Calculator coding challenge (our thanks to Tony “Tibs” Ibbs). To clone it, open a terminal window on your computer, change directory to somewhere you’re happy to put it, and enter:

git clone git@github.com:tibs/roman_adding.git

If you don’t have Git, you can download it as a zip file at

https://github.com/tibs/roman_adding

(click on the green ‘Clone or download’ link).

To run the unit tests, simply run the test_roman_adding.py file.

Make the tests fail randomly

To make this more interesting, let’s add a random calculation error into the code.

In roman_adding.py, import the random module and change the calculation from

sum = number1 + number2

to

sum = number1 + number2 + random.choice(['', 'I'])

in 1 out of every 2 runs, this will randomly add 1 (that is, I) to the result of the calculation. So 1 in 2 of the unit tests should now fail.

Step 5: Connect your app to your computer

We’re going to use the Uplink to allow Anvil to trigger test runs on your machine from the web app.

First, configure your app to use the Uplink by clicking on Uplink... in the Gear Menu Gear Menu Icon

and clicking the ’enable’ button in the modal that comes up:

A random code will be displayed that allows your local script to identify your Anvil app.

On your computer, install the Anvil server module using pip:

pip install anvil-uplink

(As always, I suggest you do this in a Python Virtual Environment.)

Now create a file in the Roman Adding repo called something like connect_to_anvil.py. Add these lines at the top

import anvil.server

anvil.server.connect("<The Uplink key for your app>")

Where <The Uplink key for your app> is the key you got from the Uplink... modal.

Within this script, define a function to call from your app:

@anvil.server.callable
def run_tests(times_to_run):
  print('Running tests...')
  results = []
  for i in range(times_to_run):
    print("Run number {}".format(i))
  return results
  
anvil.server.wait_forever()

The @anvil.server.callable makes it possible to call this function from the browser code. In the Anvil Editor, add an anvil.server.call to the top of your click handler:

import anvil.server
# ...
  def run_tests_button_click(self, **event_args):
    """This method is called when the button is clicked"""
    results = anvil.server.call('run_tests', self.run_number_box.text)

Run connect_to_anvil.py. You should see something like

Connecting to wss://anvil.works/uplink
Anvil websocket open
Authenticated OK

Now when you click on the Run Tests button, your script will print this to your terminal:

Running tests...
Run number 0
Run number 1

Step 6: Running the tests from your app

Now to make your Uplink script actually run the unit tests.

Add a call to the unittest module inside the loop in the run_tests function and append the results to the results list:

import unittest
from datetime import datetime

  # .. and inside the run_tests function ...
  for i in range(times_to_run):
     # run the tests
     result = unittest.main(module='test_roman_adding', exit=False).result
     
     # unpack the results a bit
     failed = len(result.failures)
     errors = len(result.errors)
     passed = result.testsRun - failed - errors

      # Create an entry in the results list
     results.append({
         'date_time': datetime.now(),
         'tests_run': result.testsRun,
         'passed': passed,
         'failed': failed,
         'errors': errors,
     })

Now in your app, you replace the code that concocts fake data with the call to your actual test runner:

  def run_tests_button_click(self, **event_args):
    """This method is called when the button is clicked"""
    results = anvil.server.call('run_tests', self.run_number_box.text)
    self.repeating_panel_1.items = results + self.repeating_panel_1.items

You’ve now got a web app that runs tests on a remote machine and displays the results!

Step 7: Store the results persistently

Currently, when the app is reloaded in the browser, the existing test results are cleared from memory. It would be nice if your test results could persist.

In the Editor, click on the plus next to ‘Services’ and add the Data Tables Service. Add a table named test_results. Add a ‘Date and Time’ column called date_time and ‘Number’ columns called tests_run, passed, failed and errors.

We’ll access this table from a Server Module. A Server Module is Python code that runs in a Python runtime on a server managed by us. Click on the plus next to ‘Server Modules’ in the Editor. You’ll see a code editor with a yellow background, denoting the Anvil server environment.

Functions in here can be decorated as @anvil.server.callable just like functions in your Uplink script. Write a simple function to get the data from the Data Table.

@anvil.server.callable
def get_test_results():
  return app_tables.test_results.search(tables.order_by('date_time', ascending=False))

To put the data into the Data Table, we’ll use another tiny function:

@anvil.server.callable
def store_test_results(results):
  for result in results:
    app_tables.test_results.add_row(
      date_time=result['date_time'],
      tests_run=result['tests_run'],
      passed=result['passed'],
      failed=result['failed'],
      errors=result['errors'],
    )

(If you’re quite familiar with Python you might spot that you could just do app_tables.test_results.add_row(**result))

So we have functions for storing test results and retrieving them. Since they are anvil.server.callable, they can be called from anywhere, so both the browser and connect_to_anvil.py can use them to persist data.

When the connect_to_anvil.py script has finished running tests, it needs to run the store_test_result function. Add that call in now:

@anvil.server.callable
def run_tests(times_to_run):
  # ... run the tests, then ...
  anvil.server.call('store_test_results', results)
  return results

And when the app starts up, it should retrieve the historical test results from the Data Table:

class Form1(Form1Template):

  def __init__(self, **properties):
    # ...
    self.repeating_panel_1.items = list(anvil.server.call('get_test_results'))

Now your test results are stored between sessions. Try launching a few test runs with your app and refreshing the page. The previous runs are still there, and when you trigger new runs they get added to the list.

And we’re done

And that’s it! You’ve just built the foundation of a Continuous Integration platform in Anvil.

It connects to a remote machine, runs a script that you might typically find in a build-test-deploy pipeline, and stores data about the runs so users can see what’s going on.

Connecting your app to an arbitrary Python process opens up an infinity of possibilities.

You could construct a more elastic build system by spinning up cloud servers (using, say, the AWS Boto3 module from Anvil’s Server Modules) and connecting to them with the Uplink to run build scripts.

You could connect to an Internet of Things gateway and use your app to manage your devices.

You can even run an interactive terminal session in the browser.

If you can do it in Python, you can connect it to your Anvil app.

Clone the finished app

Every app in Anvil has a URL that allows it to be imported by another Anvil user.

Click the following link to clone the finished app from this workshop.

If you want to run the cloned version, you need to enable the Uplink as detailed in Step 5.

Your app is live on the internet already (find out more).

Extensions

If you’ve got this far, you might enjoy figuring out how to grow your app further. Some things you might like to try include:

  • Storing the ID of the commit being tested.
  • Running the tests every time a commit is made, and pushing the results to your app.
  • Adding a more detailed breakdown of test results.
  • Adding linting results.
  • Adding test coverage results.
  • Adding a ‘deploy’ button to the app and watching the progress of a deployment.
  • Controlling other hardware to visually display test status, for example using an Easter Island Tiki head that snorts dry ice.

Alternatively, take a look at the TODO list workshop and the Data Dashboard workshop.