Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gRPC and test fixes #1712

Merged
merged 4 commits into from
Nov 9, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions js/init_and_modules_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*
*
* k6 - a next-generation load testing tool
* Copyright (C) 2020 Load Impact
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

package js_test

import (
"context"
"net/url"
"testing"
"time"

"github.com/loadimpact/k6/js"
"github.com/loadimpact/k6/js/common"
"github.com/loadimpact/k6/js/internal/modules"
"github.com/loadimpact/k6/lib"
"github.com/loadimpact/k6/lib/testutils"
"github.com/loadimpact/k6/loader"
"github.com/loadimpact/k6/stats"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/guregu/null.v3"
)

type CheckModule struct {
t testing.TB
initCtxCalled int
vuCtxCalled int
}

func (cm *CheckModule) InitCtx(ctx context.Context) {
cm.initCtxCalled++
assert.NotNil(cm.t, common.GetRuntime(ctx))
assert.NotNil(cm.t, common.GetInitEnv(ctx))
assert.Nil(cm.t, lib.GetState(ctx))
}

func (cm *CheckModule) VuCtx(ctx context.Context) {
cm.vuCtxCalled++
assert.NotNil(cm.t, common.GetRuntime(ctx))
assert.Nil(cm.t, common.GetInitEnv(ctx))
assert.NotNil(cm.t, lib.GetState(ctx))
}

func TestNewJSRunnerWithCustomModule(t *testing.T) {
checkModule := &CheckModule{t: t}
modules.Register("k6/check", checkModule)
Comment on lines +63 to +64
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like that this gives us a simple way to bridge JS with Go test code. This approach might help with some flaky tests that currently rely on timing instead.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We just have to remember to use different packages for registering new k6 modules that help with testing... Given the singleton nature of the module registry, we don't want "pollution"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that could be a problem. Should we add some kind of Deregister mechanism so that each test can clean up after itself? Since it's in internal we don't have to make it public and could only use it in tests.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it shouldn't really matter if such tests are in their own package, but sure, an internal de-registering function can be added in the future


script := `
var check = require("k6/check");
check.initCtx();

module.exports.options = { vus: 1, iterations: 1 };
module.exports.default = function() {
check.vuCtx();
};
`

logger := testutils.NewLogger(t)
rtOptions := lib.RuntimeOptions{CompatibilityMode: null.StringFrom("base")}
runner, err := js.New(
logger,
&loader.SourceData{
URL: &url.URL{Path: "blah", Scheme: "file"},
Data: []byte(script),
},
map[string]afero.Fs{"file": afero.NewMemMapFs(), "https": afero.NewMemMapFs()},
rtOptions,
)
require.NoError(t, err)
assert.Equal(t, checkModule.initCtxCalled, 1)
assert.Equal(t, checkModule.vuCtxCalled, 0)

vu, err := runner.NewVU(1, make(chan stats.SampleContainer, 100))
require.NoError(t, err)
assert.Equal(t, checkModule.initCtxCalled, 2)
assert.Equal(t, checkModule.vuCtxCalled, 0)

vuCtx, vuCancel := context.WithTimeout(context.Background(), 5*time.Second)
defer vuCancel()

activeVU := vu.Activate(&lib.VUActivationParams{RunContext: vuCtx})
require.NoError(t, activeVU.RunOnce())
assert.Equal(t, checkModule.initCtxCalled, 2)
assert.Equal(t, checkModule.vuCtxCalled, 1)
require.NoError(t, activeVU.RunOnce())
assert.Equal(t, checkModule.initCtxCalled, 2)
assert.Equal(t, checkModule.vuCtxCalled, 2)

arc := runner.MakeArchive()
assert.Equal(t, checkModule.initCtxCalled, 2) // shouldn't change, we're not executing the init context again
assert.Equal(t, checkModule.vuCtxCalled, 2)

runnerFromArc, err := js.NewFromArchive(logger, arc, rtOptions)
require.NoError(t, err)
assert.Equal(t, checkModule.initCtxCalled, 3) // changes because we need to get the exported functions
assert.Equal(t, checkModule.vuCtxCalled, 2)
vuFromArc, err := runnerFromArc.NewVU(2, make(chan stats.SampleContainer, 100))
require.NoError(t, err)
assert.Equal(t, checkModule.initCtxCalled, 4)
assert.Equal(t, checkModule.vuCtxCalled, 2)
activeVUFromArc := vuFromArc.Activate(&lib.VUActivationParams{RunContext: vuCtx})
require.NoError(t, activeVUFromArc.RunOnce())
assert.Equal(t, checkModule.initCtxCalled, 4)
assert.Equal(t, checkModule.vuCtxCalled, 3)
require.NoError(t, activeVUFromArc.RunOnce())
assert.Equal(t, checkModule.initCtxCalled, 4)
assert.Equal(t, checkModule.vuCtxCalled, 4)
}
6 changes: 4 additions & 2 deletions js/modules/k6/grpc/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,10 @@ type Client struct {
conn *grpc.ClientConn
}

// NewClient creates a new gPRC client to invoke RPC methods.
func (*GRPC) NewClient(ctxPtr *context.Context) interface{} {
// XClient represents the Client constructor (e.g. `new grpc.Client()`) and
// creates a new gPRC client object that can load protobuf definitions, connect
// to servers and invoke RPC methods.
func (*GRPC) XClient(ctxPtr *context.Context) interface{} {
rt := common.GetRuntime(*ctxPtr)

return common.Bind(rt, &Client{}, ctxPtr)
Expand Down
2 changes: 1 addition & 1 deletion js/modules/k6/grpc/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ func TestClient(t *testing.T) {

t.Run("New", func(t *testing.T) {
_, err := common.RunString(rt, `
var client = grpc.newClient();
var client = new grpc.Client();
if (!client) throw new Error("no client created")
`)
assert.NoError(t, err)
Expand Down
9 changes: 5 additions & 4 deletions js/modules/k6/http/request_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -541,12 +541,13 @@ func TestRequestAndBatch(t *testing.T) {
})
}
t.Run("ocsp_stapled_good", func(t *testing.T) {
_, err := common.RunString(rt, `
var res = http.request("GET", "https://www.microsoft.com/en-us/");
website := "https://www.wikipedia.org/"
_, err := common.RunString(rt, fmt.Sprintf(`
var res = http.request("GET", "%s");
if (res.ocsp.status != http.OCSP_STATUS_GOOD) { throw new Error("wrong ocsp stapled response status: " + res.ocsp.status); }
`)
`, website))
assert.NoError(t, err)
assertRequestMetricsEmitted(t, stats.GetBufferedSamples(samples), "GET", "https://www.microsoft.com/en-us/", "", 200, "")
assertRequestMetricsEmitted(t, stats.GetBufferedSamples(samples), "GET", website, "", 200, "")
})
})
t.Run("Invalid", func(t *testing.T) {
Expand Down
175 changes: 175 additions & 0 deletions js/runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import (
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/test/grpc_testing"
"gopkg.in/guregu/null.v3"

"github.com/loadimpact/k6/core"
Expand All @@ -58,6 +59,7 @@ import (
"github.com/loadimpact/k6/lib/testutils"
"github.com/loadimpact/k6/lib/testutils/httpmultibin"
"github.com/loadimpact/k6/lib/types"
"github.com/loadimpact/k6/loader"
"github.com/loadimpact/k6/stats"
"github.com/loadimpact/k6/stats/dummy"
)
Expand Down Expand Up @@ -1801,3 +1803,176 @@ func TestVUPanic(t *testing.T) {
})
}
}

type multiFileTestCase struct {
fses map[string]afero.Fs
rtOpts lib.RuntimeOptions
cwd string
script string
expInitErr bool
expVUErr bool
samples chan stats.SampleContainer
}

func runMultiFileTestCase(t *testing.T, tc multiFileTestCase, tb *httpmultibin.HTTPMultiBin) {
logger := testutils.NewLogger(t)
runner, err := New(
logger,
&loader.SourceData{
URL: &url.URL{Path: tc.cwd + "/script.js", Scheme: "file"},
Data: []byte(tc.script),
},
tc.fses,
tc.rtOpts,
)
if tc.expInitErr {
require.Error(t, err)
return
}
require.NoError(t, err)

options := runner.GetOptions()
require.Empty(t, options.Validate())

vu, err := runner.NewVU(1, tc.samples)
require.NoError(t, err)

jsVU, ok := vu.(*VU)
require.True(t, ok)
jsVU.state.Dialer = tb.Dialer
jsVU.state.TLSConfig = tb.TLSClientConfig

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
activeVU := vu.Activate(&lib.VUActivationParams{RunContext: ctx})

err = activeVU.RunOnce()
if tc.expVUErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}

arc := runner.MakeArchive()
runnerFromArc, err := NewFromArchive(logger, arc, tc.rtOpts)
require.NoError(t, err)
vuFromArc, err := runnerFromArc.NewVU(2, tc.samples)
require.NoError(t, err)
jsVUFromArc, ok := vuFromArc.(*VU)
require.True(t, ok)
jsVUFromArc.state.Dialer = tb.Dialer
jsVUFromArc.state.TLSConfig = tb.TLSClientConfig
activeVUFromArc := jsVUFromArc.Activate(&lib.VUActivationParams{RunContext: ctx})
err = activeVUFromArc.RunOnce()
if tc.expVUErr {
require.Error(t, err)
return
}
require.NoError(t, err)
}

func TestComplicatedFileImportsForGRPC(t *testing.T) {
t.Parallel()
tb := httpmultibin.NewHTTPMultiBin(t)
defer tb.Cleanup()

tb.GRPCStub.UnaryCallFunc = func(ctx context.Context, sreq *grpc_testing.SimpleRequest) (
*grpc_testing.SimpleResponse, error,
) {
return &grpc_testing.SimpleResponse{
Username: "foo",
}, nil
}

fs := afero.NewMemMapFs()
protoFile, err := ioutil.ReadFile("../vendor/google.golang.org/grpc/test/grpc_testing/test.proto")
require.NoError(t, err)
require.NoError(t, afero.WriteFile(fs, "/path/to/service.proto", protoFile, 0644))
require.NoError(t, afero.WriteFile(fs, "/path/to/same-dir.proto", []byte(
`syntax = "proto3";package whatever;import "service.proto";`,
), 0644))
require.NoError(t, afero.WriteFile(fs, "/path/subdir.proto", []byte(
`syntax = "proto3";package whatever;import "to/service.proto";`,
), 0644))
require.NoError(t, afero.WriteFile(fs, "/path/to/abs.proto", []byte(
`syntax = "proto3";package whatever;import "/path/to/service.proto";`,
), 0644))

grpcTestCase := func(expInitErr, expVUErr bool, cwd, loadCode string) multiFileTestCase {
script := tb.Replacer.Replace(fmt.Sprintf(`
var grpc = require('k6/net/grpc');
var client = new grpc.Client();

%s // load statements

exports.default = function() {
client.connect('GRPCBIN_ADDR', {timeout: '3s'});
var resp = client.invoke('grpc.testing.TestService/UnaryCall', {})
if (!resp.message || resp.error || resp.message.username !== 'foo') {
throw new Error('unexpected response message: ' + JSON.stringify(resp.message))
}
}
`, loadCode))

return multiFileTestCase{
fses: map[string]afero.Fs{"file": fs, "https": afero.NewMemMapFs()},
rtOpts: lib.RuntimeOptions{CompatibilityMode: null.NewString("base", true)},
samples: make(chan stats.SampleContainer, 100),
cwd: cwd, expInitErr: expInitErr, expVUErr: expVUErr, script: script,
}
}

testCases := []multiFileTestCase{
grpcTestCase(false, true, "/", `/* no grpc loads */`), // exp VU error with no proto files loaded

// Init errors when the protobuf file can't be loaded
grpcTestCase(true, false, "/", `client.load(null, 'service.proto');`),
grpcTestCase(true, false, "/", `client.load(null, '/wrong/path/to/service.proto');`),
grpcTestCase(true, false, "/", `client.load(['/', '/path/'], 'service.proto');`),

// Direct imports of service.proto
grpcTestCase(false, false, "/", `client.load(null, '/path/to/service.proto');`), // full path should be fine
grpcTestCase(false, false, "/path/to/", `client.load([], 'service.proto');`), // file name from same folder
grpcTestCase(false, false, "/", `client.load(['./path//to/'], 'service.proto');`),
grpcTestCase(false, false, "/path/", `client.load(['./to/'], 'service.proto');`),

grpcTestCase(false, false, "/whatever", `client.load(['/path/to/'], 'service.proto');`), // with import paths
grpcTestCase(false, false, "/path", `client.load(['/', '/path/to/'], 'service.proto');`), // with import paths
grpcTestCase(false, false, "/whatever", `client.load(['../path/to/'], 'service.proto');`),

// Import another file that imports "service.proto" directly
grpcTestCase(true, false, "/", `client.load([], '/path/to/same-dir.proto');`),
grpcTestCase(true, false, "/path/", `client.load([], 'to/same-dir.proto');`),
grpcTestCase(true, false, "/", `client.load(['/path/'], 'to/same-dir.proto');`),
grpcTestCase(false, false, "/path/to/", `client.load([], 'same-dir.proto');`),
grpcTestCase(false, false, "/", `client.load(['/path/to/'], 'same-dir.proto');`),
grpcTestCase(false, false, "/whatever", `client.load(['/other', '/path/to/'], 'same-dir.proto');`),
grpcTestCase(false, false, "/", `client.load(['./path//to/'], 'same-dir.proto');`),
grpcTestCase(false, false, "/path/", `client.load(['./to/'], 'same-dir.proto');`),
grpcTestCase(false, false, "/whatever", `client.load(['../path/to/'], 'same-dir.proto');`),

// Import another file that imports "to/service.proto" directly
grpcTestCase(true, false, "/", `client.load([], '/path/to/subdir.proto');`),
grpcTestCase(false, false, "/path/", `client.load([], 'subdir.proto');`),
grpcTestCase(false, false, "/", `client.load(['/path/'], 'subdir.proto');`),
grpcTestCase(false, false, "/", `client.load(['./path/'], 'subdir.proto');`),
grpcTestCase(false, false, "/whatever", `client.load(['/other', '/path/'], 'subdir.proto');`),
grpcTestCase(false, false, "/whatever", `client.load(['../other', '../path/'], 'subdir.proto');`),

// Import another file that imports "/path/to/service.proto" directly
grpcTestCase(true, false, "/", `client.load(['/path'], '/path/to/abs.proto');`),
grpcTestCase(false, false, "/", `client.load([], '/path/to/abs.proto');`),
grpcTestCase(false, false, "/whatever", `client.load(['/'], '/path/to/abs.proto');`),
}

for i, tc := range testCases {
i, tc := i, tc
t.Run(fmt.Sprintf("TestCase_%d", i), func(t *testing.T) {
t.Logf(
"CWD: %s, expInitErr: %t, expVUErr: %t, script injected with: `%s`",
tc.cwd, tc.expInitErr, tc.expVUErr, tc.script,
)
runMultiFileTestCase(t, tc, tb)
})
}
}
2 changes: 1 addition & 1 deletion samples/grpc.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import grpc from 'k6/net/grpc';
import { check } from "k6";

let client = grpc.newClient();
let client = new grpc.Client();
client.load([], "samples/grpc_server/route_guide.proto")


Expand Down