diff --git a/handlers/namespaces.go b/handlers/namespaces.go new file mode 100644 index 0000000..43da31d --- /dev/null +++ b/handlers/namespaces.go @@ -0,0 +1,88 @@ +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package handlers + +import ( + "fmt" + + "github.com/m3db/m3cluster/kv" + "github.com/m3db/m3metrics/generated/proto/schema" +) + +// Namespaces returns the version and the persisted namespaces in kv store. +func Namespaces(store kv.Store, namespacesKey string) (int, *schema.Namespaces, error) { + value, err := store.Get(namespacesKey) + if err != nil { + return 0, nil, err + } + + version := value.Version() + var namespaces schema.Namespaces + if err := value.Unmarshal(&namespaces); err != nil { + return 0, nil, err + } + + return version, &namespaces, nil +} + +// ValidateNamespace validates whether a given namespace exists. +func ValidateNamespace(store kv.Store, namespacesKey string, namespaceName string) (int, *schema.Namespaces, *schema.Namespace, error) { + namespacesVersion, namespaces, err := Namespaces(store, namespacesKey) + if err != nil { + return 0, nil, nil, fmt.Errorf("could not read namespaces data: %v", err) + } + ns, err := Namespace(namespaces, namespaceName) + if err != nil { + return 0, nil, nil, fmt.Errorf("error finding namespace with name %s: %v", namespaceName, err) + } + if ns == nil { + return 0, nil, nil, fmt.Errorf("namespace %s doesn't exist", namespaceName) + } + if len(ns.Snapshots) == 0 { + return 0, nil, nil, fmt.Errorf("namespace %s has no snapshots", namespaceName) + } + if ns.Snapshots[len(ns.Snapshots)-1].Tombstoned { + return 0, nil, nil, fmt.Errorf("namespace %s is tombstoned", namespaceName) + } + return namespacesVersion, namespaces, ns, nil +} + +// Namespace returns the namespace with a given name, or an error if there are +// multiple matches. +func Namespace(namespaces *schema.Namespaces, namespaceName string) (*schema.Namespace, error) { + var namespace *schema.Namespace + for _, ns := range namespaces.Namespaces { + if ns.Name != namespaceName { + continue + } + if namespace == nil { + namespace = ns + } else { + return nil, errMultipleMatches + } + } + + if namespace == nil { + return nil, kv.ErrNotFound + } + + return namespace, nil +} diff --git a/handlers/namespaces_test.go b/handlers/namespaces_test.go new file mode 100644 index 0000000..9b0092a --- /dev/null +++ b/handlers/namespaces_test.go @@ -0,0 +1,134 @@ +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package handlers + +import ( + "testing" + + "github.com/m3db/m3cluster/kv" + "github.com/m3db/m3cluster/kv/mem" + "github.com/m3db/m3metrics/generated/proto/schema" + "github.com/stretchr/testify/require" +) + +const ( + testNamespaceKey = "testKey" +) + +var ( + testNamespaces = &schema.Namespaces{ + Namespaces: []*schema.Namespace{ + &schema.Namespace{ + Name: "fooNs", + Snapshots: []*schema.NamespaceSnapshot{ + &schema.NamespaceSnapshot{ + ForRulesetVersion: 1, + Tombstoned: false, + }, + &schema.NamespaceSnapshot{ + ForRulesetVersion: 2, + Tombstoned: false, + }, + }, + }, + &schema.Namespace{ + Name: "barNs", + Snapshots: []*schema.NamespaceSnapshot{ + &schema.NamespaceSnapshot{ + ForRulesetVersion: 1, + Tombstoned: false, + }, + &schema.NamespaceSnapshot{ + ForRulesetVersion: 2, + Tombstoned: true, + }, + }, + }, + }, + } + + badNamespaces = &schema.Namespaces{ + Namespaces: []*schema.Namespace{ + &schema.Namespace{Name: "fooNs", Snapshots: nil}, + &schema.Namespace{Name: "fooNs", Snapshots: nil}, + }, + } +) + +func TestNamespace(t *testing.T) { + res, err := Namespace(testNamespaces, "barNs") + require.NoError(t, err) + require.EqualValues(t, testNamespaces.Namespaces[1], res) +} + +func TestNamespaceError(t *testing.T) { + res, err := Namespace(badNamespaces, "blah") + require.Error(t, err) + require.Equal(t, err, kv.ErrNotFound) + require.Nil(t, res) + + res, err = Namespace(badNamespaces, "fooNs") + require.Error(t, err) + require.Equal(t, err, errMultipleMatches) + require.Nil(t, res) +} + +func TestNamespaces(t *testing.T) { + store := mem.NewStore() + store.Set(testNamespaceKey, testNamespaces) + _, s, err := Namespaces(store, testNamespaceKey) + require.NoError(t, err) + require.NotNil(t, s.Namespaces) +} + +func TestNamespacesError(t *testing.T) { + store := mem.NewStore() + store.Set(testNamespaceKey, &schema.RollupRule{Uuid: "x"}) + _, s, err := Namespaces(store, testNamespaceKey) + require.Error(t, err) + require.Nil(t, s) +} + +func TestValidateNamespace(t *testing.T) { + store := mem.NewStore() + store.Set(testNamespaceKey, testNamespaces) + v, s, ns, err := ValidateNamespace(store, testNamespaceKey, "fooNs") + require.Equal(t, v, 1) + require.NoError(t, err) + require.NotNil(t, s.Namespaces, nil) + require.Equal(t, ns.Name, "fooNs") +} + +func TestValidateNamespaceDNE(t *testing.T) { + store := mem.NewStore() + store.Set(testNamespaceKey, testNamespaces) + _, _, _, err := ValidateNamespace(store, testNamespaceKey, "blah") + require.Error(t, err) + require.Contains(t, err.Error(), "not found") +} + +func TestValidateNamespaceTombstoned(t *testing.T) { + store := mem.NewStore() + store.Set(testNamespaceKey, testNamespaces) + _, _, _, err := ValidateNamespace(store, testNamespaceKey, "barNs") + require.Error(t, err) + require.Contains(t, err.Error(), "tombstoned") +} diff --git a/handlers/ruleset.go b/handlers/ruleset.go new file mode 100644 index 0000000..b6bf13e --- /dev/null +++ b/handlers/ruleset.go @@ -0,0 +1,112 @@ +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package handlers + +import ( + "errors" + "fmt" + + "github.com/m3db/m3cluster/kv" + "github.com/m3db/m3metrics/generated/proto/schema" +) + +var ( + errMultipleMatches = errors.New("more than one match found") +) + +// Rule returns the rule with a given name, or an error if there are mutliple matches +func Rule(ruleSet *schema.RuleSet, ruleName string) (*schema.MappingRule, *schema.RollupRule, error) { + var ( + mappingRule *schema.MappingRule + rollupRule *schema.RollupRule + ) + for _, mr := range ruleSet.MappingRules { + if len(mr.Snapshots) == 0 { + continue + } + + latestSnapshot := mr.Snapshots[len(mr.Snapshots)-1] + name := latestSnapshot.Name + if name != ruleName || latestSnapshot.Tombstoned { + continue + } + if mappingRule == nil { + mappingRule = mr + } else { + return nil, nil, errMultipleMatches + } + } + for _, rr := range ruleSet.RollupRules { + if len(rr.Snapshots) == 0 { + continue + } + latestSnapshot := rr.Snapshots[len(rr.Snapshots)-1] + name := latestSnapshot.Name + if name != ruleName || latestSnapshot.Tombstoned { + continue + } + if rollupRule == nil { + rollupRule = rr + } else { + return nil, nil, errMultipleMatches + } + } + if mappingRule != nil && rollupRule != nil { + return nil, nil, errMultipleMatches + } + + if mappingRule == nil && rollupRule == nil { + return nil, nil, kv.ErrNotFound + } + return mappingRule, rollupRule, nil +} + +// RuleSet returns the version and the persisted ruleset data in kv store. +func RuleSet(store kv.Store, ruleSetKey string) (int, *schema.RuleSet, error) { + value, err := store.Get(ruleSetKey) + if err != nil { + return 0, nil, err + } + version := value.Version() + var ruleSet schema.RuleSet + if err := value.Unmarshal(&ruleSet); err != nil { + return 0, nil, err + } + + return version, &ruleSet, nil +} + +// RuleSetKey returns the ruleset key given the namespace name. +func RuleSetKey(keyFmt string, namespace string) string { + return fmt.Sprintf(keyFmt, namespace) +} + +// ValidateRuleSet validates that a valid RuleSet exists in that keyspace. +func ValidateRuleSet(store kv.Store, ruleSetKey string) (int, *schema.RuleSet, error) { + ruleSetVersion, ruleSet, err := RuleSet(store, ruleSetKey) + if err != nil { + return 0, nil, fmt.Errorf("could not read ruleSet data for key %s: %v", ruleSetKey, err) + } + if ruleSet.Tombstoned { + return 0, nil, fmt.Errorf("ruleset %s is tombstoned", ruleSetKey) + } + return ruleSetVersion, ruleSet, nil +} diff --git a/handlers/ruleset_test.go b/handlers/ruleset_test.go new file mode 100644 index 0000000..b8b4270 --- /dev/null +++ b/handlers/ruleset_test.go @@ -0,0 +1,400 @@ +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package handlers + +import ( + "testing" + "time" + + "github.com/m3db/m3cluster/kv/mem" + "github.com/m3db/m3metrics/generated/proto/schema" + "github.com/stretchr/testify/require" +) + +const ( + testKeyFmt = "rules/%s" + testNamespace = "ns" +) + +var ( + testRuleSet = &schema.RuleSet{ + Uuid: "ruleset", + Namespace: "namespace", + CreatedAt: 1234, + LastUpdatedAt: 5678, + Tombstoned: true, + CutoverTime: 34923, + MappingRules: []*schema.MappingRule{ + &schema.MappingRule{ + Uuid: "12669817-13ae-40e6-ba2f-33087b262c68", + Snapshots: []*schema.MappingRuleSnapshot{ + &schema.MappingRuleSnapshot{ + Name: "foo", + Tombstoned: false, + CutoverTime: 12345, + TagFilters: map[string]string{ + "tag1": "value1", + "tag2": "value2", + }, + Policies: []*schema.Policy{ + &schema.Policy{ + StoragePolicy: &schema.StoragePolicy{ + Resolution: &schema.Resolution{ + WindowSize: int64(10 * time.Second), + Precision: int64(time.Second), + }, + Retention: &schema.Retention{ + Period: int64(24 * time.Hour), + }, + }, + AggregationTypes: []schema.AggregationType{ + schema.AggregationType_P999, + }, + }, + }, + }, + &schema.MappingRuleSnapshot{ + Name: "foo", + Tombstoned: false, + CutoverTime: 67890, + TagFilters: map[string]string{ + "tag3": "value3", + "tag4": "value4", + }, + Policies: []*schema.Policy{ + &schema.Policy{ + StoragePolicy: &schema.StoragePolicy{ + Resolution: &schema.Resolution{ + WindowSize: int64(time.Minute), + Precision: int64(time.Minute), + }, + Retention: &schema.Retention{ + Period: int64(24 * time.Hour), + }, + }, + }, + &schema.Policy{ + StoragePolicy: &schema.StoragePolicy{ + Resolution: &schema.Resolution{ + WindowSize: int64(5 * time.Minute), + Precision: int64(time.Minute), + }, + Retention: &schema.Retention{ + Period: int64(48 * time.Hour), + }, + }, + }, + }, + }, + }, + }, + &schema.MappingRule{ + Uuid: "12669817-13ae-40e6-ba2f-33087b262c68", + Snapshots: []*schema.MappingRuleSnapshot{ + &schema.MappingRuleSnapshot{ + Name: "dup", + Tombstoned: false, + CutoverTime: 12345, + TagFilters: map[string]string{ + "tag1": "value1", + "tag2": "value2", + }, + Policies: []*schema.Policy{ + &schema.Policy{ + StoragePolicy: &schema.StoragePolicy{ + Resolution: &schema.Resolution{ + WindowSize: int64(10 * time.Second), + Precision: int64(time.Second), + }, + Retention: &schema.Retention{ + Period: int64(24 * time.Hour), + }, + }, + AggregationTypes: []schema.AggregationType{ + schema.AggregationType_P999, + }, + }, + }, + }, + }, + }, + }, + RollupRules: []*schema.RollupRule{ + &schema.RollupRule{ + Uuid: "12669817-13ae-40e6-ba2f-33087b262c68", + Snapshots: []*schema.RollupRuleSnapshot{ + &schema.RollupRuleSnapshot{ + Name: "foo2", + Tombstoned: false, + CutoverTime: 12345, + TagFilters: map[string]string{ + "tag1": "value1", + "tag2": "value2", + }, + Targets: []*schema.RollupTarget{ + &schema.RollupTarget{ + Name: "rName1", + Tags: []string{"rtagName1", "rtagName2"}, + Policies: []*schema.Policy{ + &schema.Policy{ + StoragePolicy: &schema.StoragePolicy{ + Resolution: &schema.Resolution{ + WindowSize: int64(10 * time.Second), + Precision: int64(time.Second), + }, + Retention: &schema.Retention{ + Period: int64(24 * time.Hour), + }, + }, + }, + }, + }, + }, + }, + &schema.RollupRuleSnapshot{ + Name: "bar", + Tombstoned: true, + CutoverTime: 67890, + TagFilters: map[string]string{ + "tag3": "value3", + "tag4": "value4", + }, + Targets: []*schema.RollupTarget{ + &schema.RollupTarget{ + Name: "rName1", + Tags: []string{"rtagName1", "rtagName2"}, + Policies: []*schema.Policy{ + &schema.Policy{ + StoragePolicy: &schema.StoragePolicy{ + Resolution: &schema.Resolution{ + WindowSize: int64(time.Minute), + Precision: int64(time.Minute), + }, + Retention: &schema.Retention{ + Period: int64(24 * time.Hour), + }, + }, + }, + &schema.Policy{ + StoragePolicy: &schema.StoragePolicy{ + Resolution: &schema.Resolution{ + WindowSize: int64(5 * time.Minute), + Precision: int64(time.Minute), + }, + Retention: &schema.Retention{ + Period: int64(48 * time.Hour), + }, + }, + AggregationTypes: []schema.AggregationType{ + schema.AggregationType_MEAN, + }, + }, + }, + }, + }, + }, + }, + }, + &schema.RollupRule{ + Uuid: "12669817-13ae-40e6-ba2f-33087b262c68", + Snapshots: []*schema.RollupRuleSnapshot{ + &schema.RollupRuleSnapshot{ + Name: "foo", + Tombstoned: false, + CutoverTime: 12345, + TagFilters: map[string]string{ + "tag1": "value1", + "tag2": "value2", + }, + Targets: []*schema.RollupTarget{ + &schema.RollupTarget{ + Name: "rName1", + Tags: []string{"rtagName1", "rtagName2"}, + Policies: []*schema.Policy{ + &schema.Policy{ + StoragePolicy: &schema.StoragePolicy{ + Resolution: &schema.Resolution{ + WindowSize: int64(10 * time.Second), + Precision: int64(time.Second), + }, + Retention: &schema.Retention{ + Period: int64(24 * time.Hour), + }, + }, + }, + }, + }, + }, + }, + &schema.RollupRuleSnapshot{ + Name: "baz", + Tombstoned: false, + CutoverTime: 67890, + TagFilters: map[string]string{ + "tag3": "value3", + "tag4": "value4", + }, + Targets: []*schema.RollupTarget{ + &schema.RollupTarget{ + Name: "rName1", + Tags: []string{"rtagName1", "rtagName2"}, + Policies: []*schema.Policy{ + &schema.Policy{ + StoragePolicy: &schema.StoragePolicy{ + Resolution: &schema.Resolution{ + WindowSize: int64(time.Minute), + Precision: int64(time.Minute), + }, + Retention: &schema.Retention{ + Period: int64(24 * time.Hour), + }, + }, + }, + &schema.Policy{ + StoragePolicy: &schema.StoragePolicy{ + Resolution: &schema.Resolution{ + WindowSize: int64(5 * time.Minute), + Precision: int64(time.Minute), + }, + Retention: &schema.Retention{ + Period: int64(48 * time.Hour), + }, + }, + AggregationTypes: []schema.AggregationType{ + schema.AggregationType_MEAN, + }, + }, + }, + }, + }, + }, + }, + }, + &schema.RollupRule{ + Uuid: "12669817-13ae-40e6-ba2f-33087b262c68", + Snapshots: []*schema.RollupRuleSnapshot{ + &schema.RollupRuleSnapshot{ + Name: "dup", + Tombstoned: false, + CutoverTime: 12345, + TagFilters: map[string]string{ + "tag1": "value1", + "tag2": "value2", + }, + Targets: []*schema.RollupTarget{ + &schema.RollupTarget{ + Name: "rName1", + Tags: []string{"rtagName1", "rtagName2"}, + Policies: []*schema.Policy{ + &schema.Policy{ + StoragePolicy: &schema.StoragePolicy{ + Resolution: &schema.Resolution{ + WindowSize: int64(10 * time.Second), + Precision: int64(time.Second), + }, + Retention: &schema.Retention{ + Period: int64(24 * time.Hour), + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } +) + +func TestRuleSetKey(t *testing.T) { + expected := "rules/ns" + actual := RuleSetKey(testKeyFmt, testNamespace) + require.Equal(t, expected, actual) +} + +func TestRuleSet(t *testing.T) { + store := mem.NewStore() + rulesSetKey := RuleSetKey(testKeyFmt, testNamespace) + store.Set(rulesSetKey, testRuleSet) + _, s, err := RuleSet(store, rulesSetKey) + require.NoError(t, err) + require.NotNil(t, s) +} + +func TestRuleSetError(t *testing.T) { + store := mem.NewStore() + rulesSetKey := RuleSetKey(testKeyFmt, testNamespace) + store.Set(rulesSetKey, &schema.Namespace{Name: "x"}) + _, s, err := RuleSet(store, "blah") + require.Error(t, err) + require.Nil(t, s) +} + +func TestValidateRuleSetTombstoned(t *testing.T) { + store := mem.NewStore() + rulesSetKey := RuleSetKey(testKeyFmt, testNamespace) + store.Set(rulesSetKey, testRuleSet) + _, _, err := ValidateRuleSet(store, rulesSetKey) + require.Error(t, err) + require.Contains(t, err.Error(), "tombstoned") +} + +func TestValidateRuleSetInvalid(t *testing.T) { + store := mem.NewStore() + rulesSetKey := RuleSetKey(testKeyFmt, testNamespace) + store.Set(rulesSetKey, nil) + _, _, err := ValidateRuleSet(store, rulesSetKey) + require.Error(t, err) + require.Contains(t, err.Error(), "could not read") +} + +func TestRule(t *testing.T) { + store := mem.NewStore() + rulesSetKey := RuleSetKey(testKeyFmt, testNamespace) + store.Set(rulesSetKey, testRuleSet) + _, r, err := RuleSet(store, rulesSetKey) + require.NoError(t, err) + + m, s, err := Rule(r, "foo") + require.Nil(t, s) + require.NoError(t, err) + require.EqualValues(t, m, testRuleSet.MappingRules[0]) + + m, s, err = Rule(r, "baz") + require.Nil(t, m) + require.NoError(t, err) + require.EqualValues(t, s, testRuleSet.RollupRules[1]) +} + +func TestRuleDup(t *testing.T) { + store := mem.NewStore() + rulesSetKey := RuleSetKey(testKeyFmt, testNamespace) + store.Set(rulesSetKey, testRuleSet) + _, r, err := RuleSet(store, rulesSetKey) + require.NoError(t, err) + + m, s, err := Rule(r, "dup") + require.Error(t, err) + require.Equal(t, errMultipleMatches, err) + require.Nil(t, m) + require.Nil(t, s) +}