github.com/Jeffail/benthos/v3@v3.65.0/lib/processor/subprocess_test.go (about)

     1  package processor
     2  
     3  import (
     4  	"os"
     5  	"path"
     6  	"reflect"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/Jeffail/benthos/v3/lib/log"
    11  	"github.com/Jeffail/benthos/v3/lib/message"
    12  	"github.com/Jeffail/benthos/v3/lib/metrics"
    13  	"github.com/stretchr/testify/assert"
    14  	"github.com/stretchr/testify/require"
    15  )
    16  
    17  func TestSubprocessWithSed(t *testing.T) {
    18  	t.Skip("disabled for now")
    19  
    20  	conf := NewConfig()
    21  	conf.Type = TypeSubprocess
    22  	conf.Subprocess.Name = "sed"
    23  	conf.Subprocess.Args = []string{"s/foo/bar/g", "-u"}
    24  
    25  	proc, err := New(conf, nil, log.Noop(), metrics.Noop())
    26  	if err != nil {
    27  		t.Skipf("Not sure if this is due to missing executable: %v", err)
    28  	}
    29  
    30  	exp := [][]byte{
    31  		[]byte(`hello bar world`),
    32  		[]byte(`hello baz world`),
    33  		[]byte(`bar`),
    34  	}
    35  	msgIn := message.New([][]byte{
    36  		[]byte(`hello foo world`),
    37  		[]byte(`hello baz world`),
    38  		[]byte(`foo`),
    39  	})
    40  	msgs, res := proc.ProcessMessage(msgIn)
    41  	if len(msgs) != 1 {
    42  		t.Fatal("Wrong count of messages")
    43  	}
    44  	if res != nil {
    45  		t.Fatalf("Non-nil result: %v", res.Error())
    46  	}
    47  
    48  	if act := message.GetAllBytes(msgs[0]); !reflect.DeepEqual(exp, act) {
    49  		t.Errorf("Wrong results: %s != %s", act, exp)
    50  	}
    51  
    52  	proc.CloseAsync()
    53  	if err := proc.WaitForClose(time.Second); err != nil {
    54  		t.Error(err)
    55  	}
    56  }
    57  
    58  func TestSubprocessWithCat(t *testing.T) {
    59  	t.Skip("disabled for now")
    60  
    61  	conf := NewConfig()
    62  	conf.Type = TypeSubprocess
    63  	conf.Subprocess.Name = "cat"
    64  
    65  	proc, err := New(conf, nil, log.Noop(), metrics.Noop())
    66  	if err != nil {
    67  		t.Skipf("Not sure if this is due to missing executable: %v", err)
    68  	}
    69  
    70  	exp := [][]byte{
    71  		[]byte(`hello bar world`),
    72  		[]byte(`hello baz world`),
    73  		[]byte(`bar`),
    74  	}
    75  	msgIn := message.New([][]byte{
    76  		[]byte(`hello bar world`),
    77  		[]byte(`hello baz world`),
    78  		[]byte(`bar`),
    79  	})
    80  	msgs, res := proc.ProcessMessage(msgIn)
    81  	if len(msgs) != 1 {
    82  		t.Fatal("Wrong count of messages")
    83  	}
    84  	if res != nil {
    85  		t.Fatalf("Non-nil result: %v", res.Error())
    86  	}
    87  
    88  	if act := message.GetAllBytes(msgs[0]); !reflect.DeepEqual(exp, act) {
    89  		t.Errorf("Wrong results: %s != %s", act, exp)
    90  	}
    91  
    92  	proc.CloseAsync()
    93  	if err := proc.WaitForClose(time.Second); err != nil {
    94  		t.Error(err)
    95  	}
    96  }
    97  
    98  func TestSubprocessLineBreaks(t *testing.T) {
    99  	t.Skip("disabled for now")
   100  
   101  	conf := NewConfig()
   102  	conf.Type = TypeSubprocess
   103  	conf.Subprocess.Name = "sed"
   104  	conf.Subprocess.Args = []string{`s/\(^$\)\|\(foo\)/bar/`, "-u"}
   105  
   106  	proc, err := New(conf, nil, log.Noop(), metrics.Noop())
   107  	if err != nil {
   108  		t.Skipf("Not sure if this is due to missing executable: %v", err)
   109  	}
   110  
   111  	exp := [][]byte{
   112  		[]byte("hello bar\nbar world"),
   113  		[]byte("hello bar bar world"),
   114  		[]byte("hello bar\nbar world\n"),
   115  		[]byte("bar"),
   116  		[]byte("hello bar\nbar\nbar world\n"),
   117  	}
   118  	msgIn := message.New([][]byte{
   119  		[]byte("hello foo\nfoo world"),
   120  		[]byte("hello foo bar world"),
   121  		[]byte("hello foo\nfoo world\n"),
   122  		[]byte(""),
   123  		[]byte("hello foo\n\nfoo world\n"),
   124  	})
   125  	msgs, res := proc.ProcessMessage(msgIn)
   126  	if len(msgs) != 1 {
   127  		t.Fatalf("Wrong count of messages %d", len(msgs))
   128  	}
   129  	if res != nil {
   130  		t.Fatalf("Non-nil result: %v", res.Error())
   131  	}
   132  
   133  	if act := message.GetAllBytes(msgs[0]); !reflect.DeepEqual(exp, act) {
   134  		t.Errorf("Wrong results: %s != %s", act, exp)
   135  	}
   136  
   137  	proc.CloseAsync()
   138  	if err := proc.WaitForClose(time.Second); err != nil {
   139  		t.Error(err)
   140  	}
   141  }
   142  
   143  func TestSubprocessWithErrors(t *testing.T) {
   144  	conf := NewConfig()
   145  	conf.Type = TypeSubprocess
   146  	conf.Subprocess.Name = "sh"
   147  	conf.Subprocess.Args = []string{"-c", "cat 1>&2"}
   148  
   149  	proc, err := New(conf, nil, log.Noop(), metrics.Noop())
   150  	if err != nil {
   151  		t.Skipf("Not sure if this is due to missing executable: %v", err)
   152  	}
   153  
   154  	msgIn := message.New([][]byte{
   155  		[]byte(`hello bar world`),
   156  	})
   157  
   158  	msgs, _ := proc.ProcessMessage(msgIn)
   159  
   160  	if !HasFailed(msgs[0].Get(0)) {
   161  		t.Errorf("Expected subprocessor to fail")
   162  	}
   163  
   164  	proc.CloseAsync()
   165  	if err := proc.WaitForClose(time.Second); err != nil {
   166  		t.Error(err)
   167  	}
   168  }
   169  
   170  func testProgram(t *testing.T, program string) string {
   171  	t.Helper()
   172  
   173  	dir := t.TempDir()
   174  
   175  	pathStr := path.Join(dir, "main.go")
   176  	require.NoError(t, os.WriteFile(pathStr, []byte(program), 0o666))
   177  
   178  	return pathStr
   179  }
   180  
   181  func TestSubprocessHappy(t *testing.T) {
   182  	filePath := testProgram(t, `package main
   183  
   184  import (
   185  	"bufio"
   186  	"encoding/binary"
   187  	"flag"
   188  	"fmt"
   189  	"log"
   190  	"os"
   191  	"strings"
   192  )
   193  
   194  func lengthPrefixedUInt32BESplitFunc(data []byte, atEOF bool) (advance int, token []byte, err error) {
   195  	const prefixBytes int = 4
   196  	if atEOF {
   197  		return 0, nil, nil
   198  	}
   199  	if len(data) < prefixBytes {
   200  		// request more data
   201  		return 0, nil, nil
   202  	}
   203  	l := binary.BigEndian.Uint32(data)
   204  	bytesToRead := int(l)
   205  
   206  	if len(data)-prefixBytes >= bytesToRead {
   207  		return prefixBytes + bytesToRead, data[prefixBytes : prefixBytes+bytesToRead], nil
   208  	} else {
   209  		// request more data
   210  		return 0, nil, nil
   211  	}
   212  }
   213  
   214  var stdinCodec *string = flag.String("stdinCodec", "lines", "format to use for input")
   215  var stdoutCodec *string = flag.String("stdoutCodec", "lines", "format for use for output")
   216  
   217  func main() {
   218  	flag.Parse()
   219  
   220  	scanner := bufio.NewScanner(os.Stdin)
   221  	if *stdinCodec == "length_prefixed_uint32_be" {
   222  		scanner.Split(lengthPrefixedUInt32BESplitFunc)
   223  	}
   224  
   225  	lenBuf := make([]byte, 4)
   226  	for scanner.Scan() {
   227  		res := strings.ToUpper(scanner.Text())
   228  		switch *stdoutCodec {
   229  			case "length_prefixed_uint32_be":
   230  				buf := []byte(res)
   231  				binary.BigEndian.PutUint32(lenBuf, uint32(len(buf)))
   232  				_, _ = os.Stdout.Write(lenBuf)
   233  				_, _ = os.Stdout.Write(buf)
   234  				break
   235  			case "netstring":
   236  				fmt.Printf("%d:%s,",len(res),res)
   237  				break
   238  			default:
   239  				fmt.Println(res)
   240  		}
   241  	}
   242  
   243  	if err := scanner.Err(); err != nil {
   244  		log.Println(err)
   245  	}
   246  }
   247  `)
   248  	f := func(formatSend string, formatRecv string, extra bool) {
   249  		conf := NewConfig()
   250  		conf.Type = TypeSubprocess
   251  		conf.Subprocess.Name = "go"
   252  		conf.Subprocess.Args = []string{"run", filePath, "-stdinCodec", formatSend, "-stdoutCodec", formatRecv}
   253  		conf.Subprocess.CodecSend = formatSend
   254  		conf.Subprocess.CodecRecv = formatRecv
   255  
   256  		proc, err := New(conf, nil, log.Noop(), metrics.Noop())
   257  		require.NoError(t, err)
   258  
   259  		exp := [][]byte{
   260  			[]byte(`FOO`),
   261  			[]byte(`FOÖ`),
   262  			[]byte(`BAR`),
   263  			[]byte(`BAZ`),
   264  		}
   265  		if extra {
   266  			exp = append(exp, []byte(``), []byte("|{O\n\r\nO}|"))
   267  		}
   268  
   269  		msgIn := message.New([][]byte{
   270  			[]byte(`foo`),
   271  			[]byte(`foö`),
   272  			[]byte(`bar`),
   273  			[]byte(`baz`),
   274  		})
   275  		if extra {
   276  			msgIn.Append(message.NewPart([]byte(``)))
   277  			msgIn.Append(message.NewPart([]byte("|{o\n\r\no}|")))
   278  		}
   279  
   280  		msgs, res := proc.ProcessMessage(msgIn)
   281  		require.Len(t, msgs, 1)
   282  		require.Nil(t, res)
   283  
   284  		for i := 0; i < msgIn.Len(); i++ {
   285  			assert.Empty(t, msgs[0].Get(i).Metadata().Get(FailFlagKey))
   286  		}
   287  		assert.Equal(t, exp, message.GetAllBytes(msgs[0]))
   288  
   289  		proc.CloseAsync()
   290  		assert.NoError(t, proc.WaitForClose(time.Second))
   291  	}
   292  	f("lines", "lines", false)
   293  	f("length_prefixed_uint32_be", "lines", false)
   294  	f("lines", "length_prefixed_uint32_be", false)
   295  	f("length_prefixed_uint32_be", "netstring", true)
   296  	f("length_prefixed_uint32_be", "length_prefixed_uint32_be", true)
   297  }