# 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.