Como testar pipelines de canal em Go

Aug 15 2020

Eu uso bastante o padrão "pipeline de canal" no Go, que se parece com isto:

// getSomeNums spits out ints onto a channel. Temperatures, pressures, doesn't matter
func getSomeNums(ch chan<- int) {
    // Imagination goes here
}

// double takes numbers from in, doubles them, and pushes them into out
func double(in <-chan int, out chan<- int) {
    for v := range in {
        out <- v * 2
    close(out)
}

source := make(chan int)
go getSomeNums(source)

doubles := make(chan int)
double(source, doubles)

O problema que encontro repetidamente é que tenho que testar vários recursos diferentes dessas funções de pipeline:

  • Coloca um valor no canal de saída quando você coloca um no canal de entrada
  • Não coloca um valor no canal de saída quando você não coloca um no canal de entrada
  • Tempo limite se o canal de saída demorar muito depois que um valor aparecer no canal de entrada
  • Fecha o canal de saída quando o canal de entrada fecha
  • Não fecha o canal de saída antes do canal de entrada ser fechado
  • Executa a transformação correta na entrada

Além disso, este é apenas um exemplo realmente simples. Casos mais típicos se parecem com este exemplo, em que estamos tentando usar sensores de temperatura redundantes para encontrar erros na saída:

// Provided we have channels for sensorA, sensorB, and sensorC
import "math"

LIMIT = 0.1   // Set acceptable variation limit between sensors to 10%

type SafeTemp struct {
    Temp float64
    isSafe bool
}

// variation returns relative error between inputs. Unfortunately, "error" was taken
func variation(a, b float64) float64 {
    return math.Abs((a - b) / (a + b))
}

// safify zips together temperatures so long as error is below LIMIT
func safify(chA, chB, chC <-chan float64, chOut chan<- SafeTemp) {
    for {
        a, aOk := <-chA
        b, bOk := <-chB
        c, cOk := <-chC

        if !(aOk && bOk && cOk) {
            close(chOut)
            return
        }

        if variation(a, b) < LIMIT && variation(b, c) < LIMIT &&
                variation(c, a) < LIMIT {
            chOut <- SafeTemp{ (a + b + c) / 3, true }
        } else {
            chOut <- SafeTemp{ 0.0, false }
        }

    }
}

Agora, o número de coisas que tenho que testar para a função pipeline ( safify) aumenta significativamente:

  • Coloca um valor no canal de saída quando você obtém um em todos os canais de entrada
  • Não coloca um valor no canal de saída quando você não consegue um em todos os canais de entrada
  • O tempo limite se esgota se o canal de saída demorar muito após as entradas em todos os três canais de entrada, mas apenas nos três
  • Fecha o canal de saída quando qualquer canal de entrada fecha
  • Não fecha o canal de saída se nenhum canal de entrada estiver fechado
  • Sinaliza como isSafese o primeiro canal variasse significativamente dos outros, com tempos limite
  • Sinaliza como isSafese o segundo canal diferisse significativamente de outros, com tempos limite
  • Sinaliza como isSafese o terceiro canal fosse significativamente diferente dos outros, com tempos limite
  • Sinaliza como isSafese todos os canais fossem significativamente diferentes dos outros, com tempos limite

Além disso, os três canais de entrada podem ficar fora de sincronia entre si, o que adiciona uma complexidade significativa ainda além da mostrada acima.

Parece que muitas dessas verificações (exceto especificamente aquelas que têm a ver com cálculos corretos) são comuns a basicamente qualquer função de pipeline de canal no estilo fan em Go, e o Halting Problem garante que temos que usar tempos limites para todos dessas operações, a menos que queiramos que a interrupção de nossos testes dependa do comportamento de interrupção e eventual deslocamento de canal das funções que estão sendo testadas.

Dado o quão semelhantes esses tipos de testes são em toda a linha, e como acabo escrevendo testes bastante semelhantes, essencialmente testando o quão bem essas funções de canal de canal estão em conformidade com as garantias básicas de função de canal de canal , em vez do comportamento das funções , repetidamente , existe algum:

  1. Um conjunto padrão de práticas em torno desses tipos de testes de confiabilidade da função de pipeline de canal OU
  2. Uma estrutura padrão ou bem reforçada ou conjunto de estruturas para testar funções nativas do canal?

Respostas

2 KarlBielefeldt Aug 16 2020 at 18:27

Você está misturando duas preocupações diferentes. Se você fez uma abstração separada para o pipeline, pode testar uma vez. Algo como (desculpe a sintaxe, não sei, vá):

func double(v int) int {
    return v * 2
}

pipeline(in, out, double)

ou

func safe(v [3]float64) SafeTemp {
    if variation(v[0], v[1]) < LIMIT && variation(v[1], v[2]) < LIMIT &&
            variation(v[2], v[0]) < LIMIT {
        return SafeTemp{ (v[0] + v[1] + v[2]) / 3, true }
    } else {
        return SafeTemp{ 0.0, false }
    }
}

pipeline(in, out, safe)

Sem o polimorfismo paramétrico, você não pode realmente fazer uma pipelineabstração totalmente genérica , então você tem que aceitar uma certa quantidade de duplicação. No entanto, você deve ser capaz de pelo menos separar as preocupações do padrão de pipeline da lógica mais específica do aplicativo.