Note

This chapter is about all my thoughts, how I came to this project and how this interpreter works in general. It’s just an informative chapter and nice to know. You can skip this chapter if you are looking for the technical documentation of cwt-cucumber and its API.

Introduction

This introduction is a summary of my reasons for starting this project and a description of how this interpreter works.

I started reading Crafting Interpreters by Robert Nystrom because I wanted to learn how interpreters work. As a side note, I can highly recommend this book, I think it is one of the best books I have ever had. In his book you go step by step through the implementation of his scripting language lox. First there is jlox, the Java version and then there is clox, the C version. Find both implementations on GitHub.

After reading and implementing the interpreter example from the books, I wanted to do a meaningful project on my own. But implementing another general-purpose scripting language didn’t feel right, because it would be more or less a copy of lox. Now consider that Cucumber is different. In Cucumber, you don’t have the freedom to write any script you want. The rules for it are pretty much set with given-when-then. In my projects from work I often use Cucumber and I like the idea. And this brought me to the idea to implement this interpreter.

So this is not really a competing project to the official Cucumber-cpp. It is an educational project for me to fully understand the C implementation of Crafting Interpreters. And maybe it will turn out to be a good project.

How I started

At first it seemed really simple compared to a scripting language, because the structure is pretty much defined by the Cucumber language. There were some tricky parts, but I guess there was always a solution. Anyway, to get my initial idea, take a look at this feature file:

Feature: feature

  Scenario: scenario
    Given first_step
    When second_step
    Then third_step

If we run this feature file with a valid implementation of these steps, cucumber will execute the three steps and evaluate them. If we were to translate this feature file into lox from Crafting Interpreters, it might look something like this:

fun feature()
{
    fun scenario()
    {
      first_step();
      second_step();
      third_step();
    }
  scenario();
}

feature();

This means that all steps are native functions (like print functions, clock or time functions, etc.). A scenario is nested within a feature function, and right after the function definition of each, we call it. And this is basically what happens. Of course there is more, like hooks, tags, failing steps, etc., which makes it more complex, but this was a start.

Crafting Interpreters clox

Lets have a look at the clox interpreter, which compiles the script into a chunk of byte code. This chunk is then executed by a virtual machine. The cool thing here is, that you can print the byte code and the stack during execution. So lets have another look and replace some native functions with prints. They represent a step now:

fun feature()
{
    fun scenario()
    {
      print "Given any initial step";
      print "When something happens";
      print "Then we evaluate something";
    }
  scenario();
}

feature();

And this is translated into bytecode.

The First Example

Lets see this by example, what happens. Consider the first example ./examples/features/1_first_scenario.feature

Feature: My first feature
  This is my cucumber-cpp hello world

  Scenario: First Scenario
    Given An empty box
    When I place 2 x "apple" in it
    Then The box contains 2 item(s)

Now if provide the define PRINT_STACK to enable the debug prints (which were very helpful during implementation) we can go through the different compiled chunks. First we start with the script chunk:

== script ==
0000    11  op_code::constant   0 '<.\examples\features\1_first_scenario.feature:2>'
0002    |  op_code::define_var  1 '.\examples\features\1_first_scenario.feature:2'
0004    |  op_code::get_var     1 '.\examples\features\1_first_scenario.feature:2'
0006    |  op_code::call        0
0008    |  op_code::func_return

And this is just the Feature. All created functions are named by their filepath and linenumber. And since we want to call the feature we have op_code::call there, which loads the previous pushed varaible .\examples\features\1_first_scenario.feature:2

Next, we have the Scenario:

== .\examples\features\1_first_scenario.feature:2 ==
0000    2  op_code::print_linebreak
0001    |  op_code::constant    1 'Feature: My first feature'
0003    |  op_code::print       0
0005    |  op_code::print_indent
0006    |  op_code::constant    0 '.\examples\features\1_first_scenario.feature:2'
0008    |  op_code::println     5
0010    11  op_code::reset_context
0011    |  op_code::hook_before 0
0013    |  op_code::constant    2 '<.\examples\features\1_first_scenario.feature:5>'
0015    |  op_code::define_var  3 '.\examples\features\1_first_scenario.feature:5'
0017    |  op_code::get_var     3 '.\examples\features\1_first_scenario.feature:5'
0019    |  op_code::call        0
0021    |  op_code::hook_after  0
0023    |  op_code::func_return

Here we start with some printing operations to the terminal and then we call the scenario itself. Again, the function name is the location at line 5: .\examples\features\1_first_scenario.feature:5. Right before and after the scenario call the hooks are invoked, when they are implemented.

And finally, we have the scenario call:

== .\examples\features\1_first_scenario.feature:5 ==
0000    5  op_code::print_linebreak
0001    |  op_code::constant    1 'Scenario: First Scenario'
0003    |  op_code::print       0
0005    |  op_code::print_indent
0006    |  op_code::constant    0 '.\examples\features\1_first_scenario.feature:5'
0008    |  op_code::println     5
0010    6  op_code::init_scenario
0011    7  op_code::jump_if_failed      17
0013    |  op_code::hook_before_step
0014    |  op_code::call_step   3 'An empty box'
0016    |  op_code::hook_after_step
0017    |  op_code::constant    3 'An empty box'
0019    |  op_code::constant    2 '.\examples\features\1_first_scenario.feature:6'
0021    |  op_code::print_step_result
0022    8  op_code::jump_if_failed      28
0024    |  op_code::hook_before_step
0025    |  op_code::call_step   5 'I place 2 x "apple" in it'
0027    |  op_code::hook_after_step
0028    |  op_code::constant    5 'I place 2 x "apple" in it'
0030    |  op_code::constant    4 '.\examples\features\1_first_scenario.feature:7'
0032    |  op_code::print_step_result
0033    11  op_code::jump_if_failed     39
0035    |  op_code::hook_before_step
0036    |  op_code::call_step   7 'The box contains 2 item(s)'
0038    |  op_code::hook_after_step
0039    |  op_code::constant    7 'The box contains 2 item(s)'
0041    |  op_code::constant    6 '.\examples\features\1_first_scenario.feature:8'
0043    |  op_code::print_step_result
0044    |  op_code::func_return

You can find all according op_codes here. From loading the variables to the jump condition jump_if_failed and the hooks before and after step. The scenario name gets resolved at runtime.