ooni-probe-cli/internal/testingx/fakefill.go
Simone Basso 5ebdeb56ca
feat: tlsping and tcpping using step-by-step (#815)
## Checklist

- [x] I have read the [contribution guidelines](https://github.com/ooni/probe-cli/blob/master/CONTRIBUTING.md)
- [x] reference issue for this pull request: https://github.com/ooni/probe/issues/2158
- [x] if you changed anything related how experiments work and you need to reflect these changes in the ooni/spec repository, please link to the related ooni/spec pull request: https://github.com/ooni/spec/pull/250

## Description

This diff refactors the codebase to reimplement tlsping and tcpping
to use the step-by-step measurements style.

See docs/design/dd-003-step-by-step.md for more information on the
step-by-step measurement style.
2022-07-01 12:22:22 +02:00

139 lines
3.8 KiB
Go

package testingx
import (
"math/rand"
"reflect"
"sync"
"time"
)
// FakeFiller fills specific data structures with random data. The only
// exception to this behaviour is time.Time, which is instead filled
// with the current time plus a small random number of seconds.
//
// We use this implementation to initialize data in our model. The code
// has been written with that in mind. It will require some hammering in
// case we extend the model with new field types.
//
// Caveat: this kind of fillter does not support filling interfaces
// and channels and other complex types. The current behavior when this
// kind of data types is encountered is to just ignore them.
//
// This struct is quite limited in scope and we can fill only the
// structures you typically send over as JSONs.
//
// As part of future work, we aim to investigate whether we can
// replace this implementation with https://go.dev/blog/fuzz-beta.
type FakeFiller struct {
// mu provides mutual exclusion
mu sync.Mutex
// Now is OPTIONAL and allows to mock the current time
Now func() time.Time
// rnd is the random number generator and is
// automatically initialized on first use
rnd *rand.Rand
}
func (ff *FakeFiller) getRandLocked() *rand.Rand {
if ff.rnd == nil {
now := time.Now
if ff.Now != nil {
now = ff.Now
}
ff.rnd = rand.New(rand.NewSource(now().UnixNano()))
}
return ff.rnd
}
func (ff *FakeFiller) getRandomString() string {
defer ff.mu.Unlock()
ff.mu.Lock()
rnd := ff.getRandLocked()
n := rnd.Intn(63) + 1
// See https://stackoverflow.com/a/31832326
var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
b := make([]rune, n)
for i := range b {
b[i] = letterRunes[rnd.Intn(len(letterRunes))]
}
return string(b)
}
func (ff *FakeFiller) getRandomInt64() int64 {
defer ff.mu.Unlock()
ff.mu.Lock()
rnd := ff.getRandLocked()
return rnd.Int63()
}
func (ff *FakeFiller) getRandomBool() bool {
defer ff.mu.Unlock()
ff.mu.Lock()
rnd := ff.getRandLocked()
return rnd.Float64() >= 0.5
}
func (ff *FakeFiller) getRandomSmallPositiveInt() int {
defer ff.mu.Unlock()
ff.mu.Lock()
rnd := ff.getRandLocked()
return int(rnd.Int63n(8)) + 1 // safe cast
}
func (ff *FakeFiller) doFill(v reflect.Value) {
for v.Type().Kind() == reflect.Ptr {
if v.IsNil() {
// if the pointer is nil, allocate an element
v.Set(reflect.New(v.Type().Elem()))
}
// switch to the element
v = v.Elem()
}
switch v.Type().Kind() {
case reflect.String:
v.SetString(ff.getRandomString())
case reflect.Int64:
v.SetInt(ff.getRandomInt64())
case reflect.Bool:
v.SetBool(ff.getRandomBool())
case reflect.Struct:
if v.Type().String() == "time.Time" {
// Implementation note: we treat the time specially
// and we avoid attempting to set its fields.
v.Set(reflect.ValueOf(time.Now().Add(
time.Duration(ff.getRandomSmallPositiveInt()) * time.Second)))
return
}
for idx := 0; idx < v.NumField(); idx++ {
ff.doFill(v.Field(idx)) // visit all fields
}
case reflect.Slice:
kind := v.Type().Elem()
total := ff.getRandomSmallPositiveInt()
for idx := 0; idx < total; idx++ {
value := reflect.New(kind) // make a new element
ff.doFill(value)
v.Set(reflect.Append(v, value.Elem())) // append to slice
}
case reflect.Map:
if v.Type().Key().Kind() != reflect.String {
panic("fakefill: we only support string key types")
}
v.Set(reflect.MakeMap(v.Type())) // we need to init the map
total := ff.getRandomSmallPositiveInt()
kind := v.Type().Elem()
for idx := 0; idx < total; idx++ {
value := reflect.New(kind)
ff.doFill(value)
v.SetMapIndex(reflect.ValueOf(ff.getRandomString()), value.Elem())
}
}
}
// Fill fills the input structure or pointer with random data.
func (ff *FakeFiller) Fill(in interface{}) {
ff.doFill(reflect.ValueOf(in))
}