mirror of https://github.com/v2ray/v2ray-core
Refine log settings
parent
32c3565681
commit
db7d48e48f
|
@ -1,12 +1,5 @@
|
||||||
package log
|
package log
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/v2ray/v2ray-core/common/platform"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AccessStatus is the status of an access request from clients.
|
// AccessStatus is the status of an access request from clients.
|
||||||
type AccessStatus string
|
type AccessStatus string
|
||||||
|
|
||||||
|
@ -15,16 +8,9 @@ const (
|
||||||
AccessRejected = AccessStatus("rejected")
|
AccessRejected = AccessStatus("rejected")
|
||||||
)
|
)
|
||||||
|
|
||||||
type accessLogger interface {
|
var (
|
||||||
Log(from, to string, status AccessStatus, reason string)
|
accessLoggerInstance logWriter = &noOpLogWriter{}
|
||||||
}
|
)
|
||||||
|
|
||||||
type noOpAccessLogger struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (this *noOpAccessLogger) Log(from, to string, status AccessStatus, reason string) {
|
|
||||||
// Swallow
|
|
||||||
}
|
|
||||||
|
|
||||||
type accessLog struct {
|
type accessLog struct {
|
||||||
From string
|
From string
|
||||||
|
@ -33,60 +19,27 @@ type accessLog struct {
|
||||||
Reason string
|
Reason string
|
||||||
}
|
}
|
||||||
|
|
||||||
type fileAccessLogger struct {
|
func (this *accessLog) String() string {
|
||||||
queue chan *accessLog
|
return this.From + " " + string(this.Status) + " " + this.To + " " + this.Reason
|
||||||
logger *log.Logger
|
|
||||||
file *os.File
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (this *fileAccessLogger) close() {
|
|
||||||
this.file.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (logger *fileAccessLogger) Log(from, to string, status AccessStatus, reason string) {
|
|
||||||
select {
|
|
||||||
case logger.queue <- &accessLog{
|
|
||||||
From: from,
|
|
||||||
To: to,
|
|
||||||
Status: status,
|
|
||||||
Reason: reason,
|
|
||||||
}:
|
|
||||||
default:
|
|
||||||
// We don't expect this to happen, but don't want to block main thread as well.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (this *fileAccessLogger) Run() {
|
|
||||||
for entry := range this.queue {
|
|
||||||
this.logger.Println(entry.From + " " + string(entry.Status) + " " + entry.To + " " + entry.Reason)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newFileAccessLogger(path string) accessLogger {
|
|
||||||
file, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Unable to create or open file (%s): %v%s", path, err, platform.LineSeparator())
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &fileAccessLogger{
|
|
||||||
queue: make(chan *accessLog, 16),
|
|
||||||
logger: log.New(file, "", log.Ldate|log.Ltime),
|
|
||||||
file: file,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var accessLoggerInstance accessLogger = &noOpAccessLogger{}
|
|
||||||
|
|
||||||
// InitAccessLogger initializes the access logger to write into the give file.
|
// InitAccessLogger initializes the access logger to write into the give file.
|
||||||
func InitAccessLogger(file string) {
|
func InitAccessLogger(file string) error {
|
||||||
logger := newFileAccessLogger(file)
|
logger, err := newFileLogWriter(file)
|
||||||
if logger != nil {
|
if err != nil {
|
||||||
go logger.(*fileAccessLogger).Run()
|
Error("Failed to create access logger on file (%s): %v", file, err)
|
||||||
accessLoggerInstance = logger
|
return err
|
||||||
}
|
}
|
||||||
|
accessLoggerInstance = logger
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Access writes an access log.
|
// Access writes an access log.
|
||||||
func Access(from, to string, status AccessStatus, reason string) {
|
func Access(from, to string, status AccessStatus, reason string) {
|
||||||
accessLoggerInstance.Log(from, to, status, reason)
|
accessLoggerInstance.Log(&accessLog{
|
||||||
|
From: from,
|
||||||
|
To: to,
|
||||||
|
Status: status,
|
||||||
|
Reason: reason,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,10 +3,10 @@ package log
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/v2ray/v2ray-core/common/serial"
|
||||||
v2testing "github.com/v2ray/v2ray-core/testing"
|
v2testing "github.com/v2ray/v2ray-core/testing"
|
||||||
"github.com/v2ray/v2ray-core/testing/assert"
|
"github.com/v2ray/v2ray-core/testing/assert"
|
||||||
)
|
)
|
||||||
|
@ -22,14 +22,15 @@ func TestAccessLog(t *testing.T) {
|
||||||
Access("test_from", "test_to", AccessAccepted, "test_reason")
|
Access("test_from", "test_to", AccessAccepted, "test_reason")
|
||||||
<-time.After(2 * time.Second)
|
<-time.After(2 * time.Second)
|
||||||
|
|
||||||
accessLoggerInstance.(*fileAccessLogger).close()
|
accessLoggerInstance.(*fileLogWriter).close()
|
||||||
accessLoggerInstance = &noOpAccessLogger{}
|
accessLoggerInstance = &noOpLogWriter{}
|
||||||
|
|
||||||
content, err := ioutil.ReadFile(filename)
|
content, err := ioutil.ReadFile(filename)
|
||||||
assert.Error(err).IsNil()
|
assert.Error(err).IsNil()
|
||||||
|
|
||||||
assert.Bool(strings.Contains(string(content), "test_from")).IsTrue()
|
contentStr := serial.StringLiteral(content)
|
||||||
assert.Bool(strings.Contains(string(content), "test_to")).IsTrue()
|
assert.String(contentStr).Contains(serial.StringLiteral("test_from"))
|
||||||
assert.Bool(strings.Contains(string(content), "test_reason")).IsTrue()
|
assert.String(contentStr).Contains(serial.StringLiteral("test_to"))
|
||||||
assert.Bool(strings.Contains(string(content), "accepted")).IsTrue()
|
assert.String(contentStr).Contains(serial.StringLiteral("test_reason"))
|
||||||
|
assert.String(contentStr).Contains(serial.StringLiteral("accepted"))
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,6 @@ package log
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -13,36 +11,25 @@ const (
|
||||||
ErrorLevel = LogLevel(3)
|
ErrorLevel = LogLevel(3)
|
||||||
)
|
)
|
||||||
|
|
||||||
type logger interface {
|
type errorLog struct {
|
||||||
WriteLog(prefix, format string, v ...interface{})
|
prefix string
|
||||||
|
format string
|
||||||
|
values []interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
type noOpLogger struct {
|
func (this *errorLog) String() string {
|
||||||
}
|
|
||||||
|
|
||||||
func (this *noOpLogger) WriteLog(prefix, format string, v ...interface{}) {
|
|
||||||
// Swallow
|
|
||||||
}
|
|
||||||
|
|
||||||
type streamLogger struct {
|
|
||||||
logger *log.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
func (this *streamLogger) WriteLog(prefix, format string, v ...interface{}) {
|
|
||||||
var data string
|
var data string
|
||||||
if v == nil || len(v) == 0 {
|
if len(this.values) == 0 {
|
||||||
data = format
|
data = this.format
|
||||||
} else {
|
} else {
|
||||||
data = fmt.Sprintf(format, v...)
|
data = fmt.Sprintf(this.format, this.values...)
|
||||||
}
|
}
|
||||||
this.logger.Println(prefix + data)
|
return this.prefix + data
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
noOpLoggerInstance logger = &noOpLogger{}
|
noOpLoggerInstance logWriter = &noOpLogWriter{}
|
||||||
streamLoggerInstance logger = &streamLogger{
|
streamLoggerInstance logWriter = newStdOutLogWriter()
|
||||||
logger: log.New(os.Stdout, "", log.Ldate|log.Ltime),
|
|
||||||
}
|
|
||||||
|
|
||||||
debugLogger = noOpLoggerInstance
|
debugLogger = noOpLoggerInstance
|
||||||
infoLogger = noOpLoggerInstance
|
infoLogger = noOpLoggerInstance
|
||||||
|
@ -74,22 +61,48 @@ func SetLogLevel(level LogLevel) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func InitErrorLogger(file string) error {
|
||||||
|
logger, err := newFileLogWriter(file)
|
||||||
|
if err != nil {
|
||||||
|
Error("Failed to create error logger on file (%s): %v", file, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
streamLoggerInstance = logger
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Debug outputs a debug log with given format and optional arguments.
|
// Debug outputs a debug log with given format and optional arguments.
|
||||||
func Debug(format string, v ...interface{}) {
|
func Debug(format string, v ...interface{}) {
|
||||||
debugLogger.WriteLog("[Debug]", format, v...)
|
debugLogger.Log(&errorLog{
|
||||||
|
prefix: "[Debug]",
|
||||||
|
format: format,
|
||||||
|
values: v,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info outputs an info log with given format and optional arguments.
|
// Info outputs an info log with given format and optional arguments.
|
||||||
func Info(format string, v ...interface{}) {
|
func Info(format string, v ...interface{}) {
|
||||||
infoLogger.WriteLog("[Info]", format, v...)
|
infoLogger.Log(&errorLog{
|
||||||
|
prefix: "[Info]",
|
||||||
|
format: format,
|
||||||
|
values: v,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Warning outputs a warning log with given format and optional arguments.
|
// Warning outputs a warning log with given format and optional arguments.
|
||||||
func Warning(format string, v ...interface{}) {
|
func Warning(format string, v ...interface{}) {
|
||||||
warningLogger.WriteLog("[Warning]", format, v...)
|
warningLogger.Log(&errorLog{
|
||||||
|
prefix: "[Warning]",
|
||||||
|
format: format,
|
||||||
|
values: v,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error outputs an error log with given format and optional arguments.
|
// Error outputs an error log with given format and optional arguments.
|
||||||
func Error(format string, v ...interface{}) {
|
func Error(format string, v ...interface{}) {
|
||||||
errorLogger.WriteLog("[Error]", format, v...)
|
errorLogger.Log(&errorLog{
|
||||||
|
prefix: "[Error]",
|
||||||
|
format: format,
|
||||||
|
values: v,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,13 +25,14 @@ func TestStreamLogger(t *testing.T) {
|
||||||
v2testing.Current(t)
|
v2testing.Current(t)
|
||||||
|
|
||||||
buffer := bytes.NewBuffer(make([]byte, 0, 1024))
|
buffer := bytes.NewBuffer(make([]byte, 0, 1024))
|
||||||
logger := &streamLogger{
|
infoLogger = &stdOutLogWriter{
|
||||||
logger: log.New(buffer, "", 0),
|
logger: log.New(buffer, "", 0),
|
||||||
}
|
}
|
||||||
logger.WriteLog("TestPrefix: ", "Test %s Format", "Stream Logger")
|
Info("Test %s Format", "Stream Logger")
|
||||||
assert.Bytes(buffer.Bytes()).Equals([]byte("TestPrefix: Test Stream Logger Format\n"))
|
assert.Bytes(buffer.Bytes()).Equals([]byte("[Info]Test Stream Logger Format\n"))
|
||||||
|
|
||||||
buffer.Reset()
|
buffer.Reset()
|
||||||
logger.WriteLog("TestPrefix: ", "Test No Format")
|
errorLogger = infoLogger
|
||||||
assert.Bytes(buffer.Bytes()).Equals([]byte("TestPrefix: Test No Format\n"))
|
Error("Test No Format")
|
||||||
|
assert.Bytes(buffer.Bytes()).Equals([]byte("[Error]Test No Format\n"))
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
package log
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/v2ray/v2ray-core/common/platform"
|
||||||
|
"github.com/v2ray/v2ray-core/common/serial"
|
||||||
|
)
|
||||||
|
|
||||||
|
func createLogger(writer io.Writer) *log.Logger {
|
||||||
|
return log.New(writer, "", log.Ldate|log.Ltime)
|
||||||
|
}
|
||||||
|
|
||||||
|
type logWriter interface {
|
||||||
|
Log(serial.String)
|
||||||
|
}
|
||||||
|
|
||||||
|
type noOpLogWriter struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *noOpLogWriter) Log(serial.String) {
|
||||||
|
// Swallow
|
||||||
|
}
|
||||||
|
|
||||||
|
type stdOutLogWriter struct {
|
||||||
|
logger *log.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func newStdOutLogWriter() logWriter {
|
||||||
|
return &stdOutLogWriter{
|
||||||
|
logger: createLogger(os.Stdout),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *stdOutLogWriter) Log(log serial.String) {
|
||||||
|
this.logger.Print(log.String() + platform.LineSeparator())
|
||||||
|
}
|
||||||
|
|
||||||
|
type fileLogWriter struct {
|
||||||
|
queue chan serial.String
|
||||||
|
logger *log.Logger
|
||||||
|
file *os.File
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *fileLogWriter) Log(log serial.String) {
|
||||||
|
select {
|
||||||
|
case this.queue <- log:
|
||||||
|
default:
|
||||||
|
// We don't expect this to happen, but don't want to block main thread as well.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *fileLogWriter) run() {
|
||||||
|
for entry := range this.queue {
|
||||||
|
this.logger.Print(entry.String() + platform.LineSeparator())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *fileLogWriter) close() {
|
||||||
|
this.file.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func newFileLogWriter(path string) (*fileLogWriter, error) {
|
||||||
|
file, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
logger := &fileLogWriter{
|
||||||
|
queue: make(chan serial.String, 16),
|
||||||
|
logger: log.New(file, "", log.Ldate|log.Ltime),
|
||||||
|
file: file,
|
||||||
|
}
|
||||||
|
go logger.run()
|
||||||
|
return logger, nil
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
routerconfig "github.com/v2ray/v2ray-core/app/router/config"
|
routerconfig "github.com/v2ray/v2ray-core/app/router/config"
|
||||||
|
"github.com/v2ray/v2ray-core/common/log"
|
||||||
v2net "github.com/v2ray/v2ray-core/common/net"
|
v2net "github.com/v2ray/v2ray-core/common/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -12,6 +13,8 @@ type ConnectionConfig interface {
|
||||||
|
|
||||||
type LogConfig interface {
|
type LogConfig interface {
|
||||||
AccessLog() string
|
AccessLog() string
|
||||||
|
ErrorLog() string
|
||||||
|
LogLevel() log.LogLevel
|
||||||
}
|
}
|
||||||
|
|
||||||
type InboundDetourConfig interface {
|
type InboundDetourConfig interface {
|
||||||
|
|
|
@ -1,9 +1,35 @@
|
||||||
package json
|
package json
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/v2ray/v2ray-core/common/log"
|
||||||
|
)
|
||||||
|
|
||||||
type LogConfig struct {
|
type LogConfig struct {
|
||||||
AccessLogValue string `json:"access"`
|
AccessLogValue string `json:"access"`
|
||||||
|
ErrorLogValue string `json:"error"`
|
||||||
|
LogLevelValue string `json:"loglevel"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (config *LogConfig) AccessLog() string {
|
func (this *LogConfig) AccessLog() string {
|
||||||
return config.AccessLogValue
|
return this.AccessLogValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *LogConfig) ErrorLog() string {
|
||||||
|
return this.ErrorLogValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *LogConfig) LogLevel() log.LogLevel {
|
||||||
|
level := strings.ToLower(this.LogLevelValue)
|
||||||
|
switch level {
|
||||||
|
case "debug":
|
||||||
|
return log.DebugLevel
|
||||||
|
case "info":
|
||||||
|
return log.InfoLevel
|
||||||
|
case "error":
|
||||||
|
return log.ErrorLevel
|
||||||
|
default:
|
||||||
|
return log.WarningLevel
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package mocks
|
||||||
import (
|
import (
|
||||||
routerconfig "github.com/v2ray/v2ray-core/app/router/config"
|
routerconfig "github.com/v2ray/v2ray-core/app/router/config"
|
||||||
routertestingconfig "github.com/v2ray/v2ray-core/app/router/config/testing"
|
routertestingconfig "github.com/v2ray/v2ray-core/app/router/config/testing"
|
||||||
|
"github.com/v2ray/v2ray-core/common/log"
|
||||||
v2net "github.com/v2ray/v2ray-core/common/net"
|
v2net "github.com/v2ray/v2ray-core/common/net"
|
||||||
"github.com/v2ray/v2ray-core/shell/point/config"
|
"github.com/v2ray/v2ray-core/shell/point/config"
|
||||||
)
|
)
|
||||||
|
@ -22,6 +23,20 @@ func (config *ConnectionConfig) Settings() interface{} {
|
||||||
|
|
||||||
type LogConfig struct {
|
type LogConfig struct {
|
||||||
AccessLogValue string
|
AccessLogValue string
|
||||||
|
ErrorLogValue string
|
||||||
|
LogLevelValue log.LogLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
func (config *LogConfig) AccessLog() string {
|
||||||
|
return config.AccessLogValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *LogConfig) ErrorLog() string {
|
||||||
|
return this.ErrorLogValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *LogConfig) LogLevel() log.LogLevel {
|
||||||
|
return this.LogLevelValue
|
||||||
}
|
}
|
||||||
|
|
||||||
type PortRange struct {
|
type PortRange struct {
|
||||||
|
@ -55,10 +70,6 @@ func (this *OutboundDetourConfig) Tag() string {
|
||||||
return this.TagValue
|
return this.TagValue
|
||||||
}
|
}
|
||||||
|
|
||||||
func (config *LogConfig) AccessLog() string {
|
|
||||||
return config.AccessLogValue
|
|
||||||
}
|
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
PortValue v2net.Port
|
PortValue v2net.Port
|
||||||
LogConfigValue *LogConfig
|
LogConfigValue *LogConfig
|
||||||
|
|
|
@ -30,6 +30,25 @@ func NewPoint(pConfig config.PointConfig) (*Point, error) {
|
||||||
var vpoint = new(Point)
|
var vpoint = new(Point)
|
||||||
vpoint.port = pConfig.Port()
|
vpoint.port = pConfig.Port()
|
||||||
|
|
||||||
|
if pConfig.LogConfig() != nil {
|
||||||
|
logConfig := pConfig.LogConfig()
|
||||||
|
if len(logConfig.AccessLog()) > 0 {
|
||||||
|
err := log.InitAccessLogger(logConfig.AccessLog())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(logConfig.ErrorLog()) > 0 {
|
||||||
|
err := log.InitErrorLogger(logConfig.ErrorLog())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.SetLogLevel(logConfig.LogLevel())
|
||||||
|
}
|
||||||
|
|
||||||
ichFactory := connhandler.GetInboundConnectionHandlerFactory(pConfig.InboundConfig().Protocol())
|
ichFactory := connhandler.GetInboundConnectionHandlerFactory(pConfig.InboundConfig().Protocol())
|
||||||
if ichFactory == nil {
|
if ichFactory == nil {
|
||||||
log.Error("Unknown inbound connection handler factory %s", pConfig.InboundConfig().Protocol())
|
log.Error("Unknown inbound connection handler factory %s", pConfig.InboundConfig().Protocol())
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package assert
|
package assert
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/v2ray/v2ray-core/common/serial"
|
"github.com/v2ray/v2ray-core/common/serial"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -31,3 +33,15 @@ func (subject *StringSubject) Equals(expectation string) {
|
||||||
subject.Fail(subject.DisplayString(), "is equal to", serial.StringLiteral(expectation))
|
subject.Fail(subject.DisplayString(), "is equal to", serial.StringLiteral(expectation))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (subject *StringSubject) Contains(substring serial.String) {
|
||||||
|
if !strings.Contains(subject.value.String(), substring.String()) {
|
||||||
|
subject.Fail(subject.DisplayString(), "contains", substring)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (subject *StringSubject) NotContains(substring serial.String) {
|
||||||
|
if strings.Contains(subject.value.String(), substring.String()) {
|
||||||
|
subject.Fail(subject.DisplayString(), "doesn't contain", substring)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue