feat: tutorial on how to write the torsf experiment (#390)

Original tracking issue for Sprint 41: https://github.com/ooni/probe/issues/1507

Follow-up work in Sprint 42 tracked by: https://github.com/ooni/probe/issues/1689
This commit is contained in:
Simone Basso
2021-06-22 00:12:03 +02:00
committed by GitHub
parent a9990ac9da
commit 520398dd8e
41 changed files with 2113 additions and 26 deletions
@@ -0,0 +1,204 @@
# Chapter II: creating an empty experiment
In this chapter we will create an empty experiment and replace
the code calling the real `torsf` experiment in `main.go` to
call our empty experiment instead.
(This file is auto-generated from the corresponding source file,
so make sure you don't edit it manually.)
## Changes in main.go
In `main.go` we will simply replace the call to the
`torsf.NewExperimentMeasurer` function with a call to
a `NewExperimentMeasurer` function that we are going
to implement as part of this chapter.
After you do this, you also need to remove the now-unneded
import of the `torsf` package.
There are no additional changes to `main.go`.
```Go
m := NewExperimentMeasurer(Config{})
```
## The torsf.go file
This file will contain the implementation of the
`NewExperimentMeasurer` function.
As usual we start with the `package` declaration and
with the few imports we need to add.
```Go
package main
import (
"context"
"time"
"github.com/ooni/probe-cli/v3/internal/engine/model"
)
```
### Data structures
Next, we define data structures.
Config contains config for the torsf experiment. As for the real
`torsf` experiment, we don't have any specific config, so we keep
the structure empty. We still need to define a `Config` struct
here, because, by convention, all OONI experiments have a `Config`.
```Go
type Config struct{}
```
Measurer is the torsf measurer. This structure implements the
`model.ExperimentMeasurer` interface, as we will see below.
Most OONI experiments have a measurer that contains as its unique
field the specific configuration. Here we do the same.
```Go
type Measurer struct {
config Config
}
```
NewExperimentMeasurer creates a new model.ExperimentMeasurer
instance for performing `torsf` measurements. This function
will just assemble a new instance of `Measurer` with the `config`
that was passed as an argument.
```Go
func NewExperimentMeasurer(config Config) model.ExperimentMeasurer {
return &Measurer{config: config}
}
```
### Implementing the model.ExperimentMeasurer.
Now it's time to implement the methods required by the `model`'s
`ExperimentMeasurer` interface.
ExperimentName implements ExperimentMeasurer.ExperimentName. This function
returns the name of the experiment. This code is used by generic code
manipulating the experiment to print the experiment name.
```Go
func (m *Measurer) ExperimentName() string {
return "torsf"
}
```
ExperimentVersion implements ExperimentMeasurer.ExperimentVersion. This
function returns the version of the experiment. This code is also used by
generic code manipulating the experiment to print the experiment version.
```Go
func (m *Measurer) ExperimentVersion() string {
return "0.1.0"
}
```
Run implements ExperimentMeasurer.Run. This is the most interesting
function, where we run the experiment proper. In the previous chapter
we learned how to call this function from a `main.go` file. Here,
instead, we're going to create a minimal stub. In the subsequent
chapters, finally, we will modify this function until it is a
minimal implementation of the `torsf` experiment.
```Go
func (m *Measurer) Run(
ctx context.Context, sess model.ExperimentSession,
measurement *model.Measurement, callbacks model.ExperimentCallbacks,
) error {
```
As you can see, this is just a stub implementation that sleeps
for one second and prints a logging message.
```Go
time.Sleep(time.Second)
sess.Logger().Info("hello from the torsf experiment!")
return nil
}
```
### Summary keys
Before concluding this chapter, we also need to create the `SummaryKeys`
for this experiment. For historical reasons, the `TestKeys` of each
experiment is an `interface{}`. Every experiment also defines a `SummaryKeys`
data structure and a `GetSummaryKeys` method to convert the opaque
result of a measurement to the summary for such an experiment.
The experiment summary is *only* used by the OONI Probe CLI.
SummaryKeys contains summary keys for this experiment. Because this is
just an illustrative tutorial, we will just include a single key, named
`IsAnomaly`. This key is not exported as JSON and is used by the OONI
Probe CLI to inform the user of whether this measurement is ordinary or
anomalous. All OONI experiments' `SummaryKeys` contain such a field.
```Go
type SummaryKeys struct {
IsAnomaly bool `json:"-"`
}
```
GetSummaryKeys implements model.ExperimentMeasurer.GetSummaryKeys. This
method just converts the `TestKeys` inside `measurement` to an instance of
the `SummaryKeys` structure. For now, we'll just implement a stub returning
fake `SummaryKeys` declaring there was no anomaly.
```Go
func (m *Measurer) GetSummaryKeys(measurement *model.Measurement) (interface{}, error) {
return &SummaryKeys{IsAnomaly: false}, nil
}
```
## Running the code
We can run the code written in this chapter as follows:
```
$ go run ./experiment/torsf/chapter02
2021/06/21 20:48:32 info hello from the torsf experiment!
{
"data_format_version": "",
"input": null,
"measurement_start_time": "",
"probe_asn": "",
"probe_cc": "",
"probe_network_name": "",
"report_id": "",
"resolver_asn": "",
"resolver_ip": "",
"resolver_network_name": "",
"software_name": "",
"software_version": "",
"test_keys": null,
"test_name": "",
"test_runtime": 0,
"test_start_time": "",
"test_version": ""
}
```
Here you see that we're printing the log message and
that the `test_keys` are `null`.
The OONI data processing popeline will not be so happy
if we pass it a `null` settings, because there is not
much interesting data in there. We will thus start filling
it in the next chapter.
@@ -0,0 +1,65 @@
package main
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"github.com/apex/log"
"github.com/ooni/probe-cli/v3/internal/engine/mockable"
"github.com/ooni/probe-cli/v3/internal/engine/model"
"golang.org/x/sys/execabs"
)
func main() {
if _, err := execabs.LookPath("tor"); err != nil {
log.Fatal("cannot find the tor executable in path")
}
tempdir, err := ioutil.TempDir("", "")
if err != nil {
log.WithError(err).Fatal("cannot create temporary directory")
}
// -=-=- StartHere -=-=-
//
// # Chapter II: creating an empty experiment
//
// In this chapter we will create an empty experiment and replace
// the code calling the real `torsf` experiment in `main.go` to
// call our empty experiment instead.
//
// (This file is auto-generated from the corresponding source file,
// so make sure you don't edit it manually.)
//
// ## Changes in main.go
//
// In `main.go` we will simply replace the call to the
// `torsf.NewExperimentMeasurer` function with a call to
// a `NewExperimentMeasurer` function that we are going
// to implement as part of this chapter.
//
// After you do this, you also need to remove the now-unneded
// import of the `torsf` package.
//
// There are no additional changes to `main.go`.
//
// ```Go
m := NewExperimentMeasurer(Config{})
// ```
// -=-=- StopHere -=-=-
ctx := context.Background()
measurement := &model.Measurement{}
callbacks := model.NewPrinterCallbacks(log.Log)
sess := &mockable.Session{
MockableLogger: log.Log,
MockableTempDir: tempdir,
}
if err = m.Run(ctx, sess, measurement, callbacks); err != nil {
log.WithError(err).Fatal("torsf experiment failed")
}
data, err := json.Marshal(measurement)
if err != nil {
log.WithError(err).Fatal("json.Marshal failed")
}
fmt.Printf("%s\n", data)
}
@@ -0,0 +1,180 @@
// -=-=- StartHere -=-=-
//
// ## The torsf.go file
//
// This file will contain the implementation of the
// `NewExperimentMeasurer` function.
//
// As usual we start with the `package` declaration and
// with the few imports we need to add.
//
// ```Go
package main
import (
"context"
"time"
"github.com/ooni/probe-cli/v3/internal/engine/model"
)
// ```
//
// ### Data structures
//
// Next, we define data structures.
// Config contains config for the torsf experiment. As for the real
// `torsf` experiment, we don't have any specific config, so we keep
// the structure empty. We still need to define a `Config` struct
// here, because, by convention, all OONI experiments have a `Config`.
//
// ```Go
type Config struct{}
// ```
//
// Measurer is the torsf measurer. This structure implements the
// `model.ExperimentMeasurer` interface, as we will see below.
//
// Most OONI experiments have a measurer that contains as its unique
// field the specific configuration. Here we do the same.
//
// ```Go
type Measurer struct {
config Config
}
// ```
// NewExperimentMeasurer creates a new model.ExperimentMeasurer
// instance for performing `torsf` measurements. This function
// will just assemble a new instance of `Measurer` with the `config`
// that was passed as an argument.
//
// ```Go
func NewExperimentMeasurer(config Config) model.ExperimentMeasurer {
return &Measurer{config: config}
}
// ```
//
// ### Implementing the model.ExperimentMeasurer.
//
// Now it's time to implement the methods required by the `model`'s
// `ExperimentMeasurer` interface.
// ExperimentName implements ExperimentMeasurer.ExperimentName. This function
// returns the name of the experiment. This code is used by generic code
// manipulating the experiment to print the experiment name.
//
// ```Go
func (m *Measurer) ExperimentName() string {
return "torsf"
}
// ```
//
// ExperimentVersion implements ExperimentMeasurer.ExperimentVersion. This
// function returns the version of the experiment. This code is also used by
// generic code manipulating the experiment to print the experiment version.
//
// ```Go
func (m *Measurer) ExperimentVersion() string {
return "0.1.0"
}
// ```
//
// Run implements ExperimentMeasurer.Run. This is the most interesting
// function, where we run the experiment proper. In the previous chapter
// we learned how to call this function from a `main.go` file. Here,
// instead, we're going to create a minimal stub. In the subsequent
// chapters, finally, we will modify this function until it is a
// minimal implementation of the `torsf` experiment.
//
// ```Go
func (m *Measurer) Run(
ctx context.Context, sess model.ExperimentSession,
measurement *model.Measurement, callbacks model.ExperimentCallbacks,
) error {
// ```
// As you can see, this is just a stub implementation that sleeps
// for one second and prints a logging message.
//
// ```Go
time.Sleep(time.Second)
sess.Logger().Info("hello from the torsf experiment!")
return nil
}
// ```
// ### Summary keys
//
// Before concluding this chapter, we also need to create the `SummaryKeys`
// for this experiment. For historical reasons, the `TestKeys` of each
// experiment is an `interface{}`. Every experiment also defines a `SummaryKeys`
// data structure and a `GetSummaryKeys` method to convert the opaque
// result of a measurement to the summary for such an experiment.
//
// The experiment summary is *only* used by the OONI Probe CLI.
// SummaryKeys contains summary keys for this experiment. Because this is
// just an illustrative tutorial, we will just include a single key, named
// `IsAnomaly`. This key is not exported as JSON and is used by the OONI
// Probe CLI to inform the user of whether this measurement is ordinary or
// anomalous. All OONI experiments' `SummaryKeys` contain such a field.
//
// ```Go
type SummaryKeys struct {
IsAnomaly bool `json:"-"`
}
// ```
//
// GetSummaryKeys implements model.ExperimentMeasurer.GetSummaryKeys. This
// method just converts the `TestKeys` inside `measurement` to an instance of
// the `SummaryKeys` structure. For now, we'll just implement a stub returning
// fake `SummaryKeys` declaring there was no anomaly.
//
// ```Go
func (m *Measurer) GetSummaryKeys(measurement *model.Measurement) (interface{}, error) {
return &SummaryKeys{IsAnomaly: false}, nil
}
// ```
//
// ## Running the code
//
// We can run the code written in this chapter as follows:
//
// ```
// $ go run ./experiment/torsf/chapter02
// 2021/06/21 20:48:32 info hello from the torsf experiment!
// {
// "data_format_version": "",
// "input": null,
// "measurement_start_time": "",
// "probe_asn": "",
// "probe_cc": "",
// "probe_network_name": "",
// "report_id": "",
// "resolver_asn": "",
// "resolver_ip": "",
// "resolver_network_name": "",
// "software_name": "",
// "software_version": "",
// "test_keys": null,
// "test_name": "",
// "test_runtime": 0,
// "test_start_time": "",
// "test_version": ""
// }
// ```
//
// Here you see that we're printing the log message and
// that the `test_keys` are `null`.
//
// The OONI data processing popeline will not be so happy
// if we pass it a `null` settings, because there is not
// much interesting data in there. We will thus start filling
// it in the next chapter.