2015-03-30 16:12:51 +00:00
|
|
|
// Copyright 2015 The Prometheus Authors
|
|
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
// you may not use this file except in compliance with the License.
|
|
|
|
// You may obtain a copy of the License at
|
|
|
|
//
|
|
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
//
|
|
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
// See the License for the specific language governing permissions and
|
|
|
|
// limitations under the License.
|
|
|
|
|
|
|
|
package promql
|
|
|
|
|
|
|
|
import (
|
2015-05-12 08:39:10 +00:00
|
|
|
"fmt"
|
2015-03-30 16:12:51 +00:00
|
|
|
"reflect"
|
|
|
|
"testing"
|
|
|
|
)
|
|
|
|
|
|
|
|
var tests = []struct {
|
2015-05-11 12:04:53 +00:00
|
|
|
input string
|
|
|
|
expected []item
|
|
|
|
fail bool
|
|
|
|
seriesDesc bool // Whether to lex a series description.
|
2015-03-30 16:12:51 +00:00
|
|
|
}{
|
|
|
|
// Test common stuff.
|
|
|
|
{
|
|
|
|
input: ",",
|
|
|
|
expected: []item{{itemComma, 0, ","}},
|
|
|
|
}, {
|
|
|
|
input: "()",
|
|
|
|
expected: []item{{itemLeftParen, 0, `(`}, {itemRightParen, 1, `)`}},
|
|
|
|
}, {
|
|
|
|
input: "{}",
|
|
|
|
expected: []item{{itemLeftBrace, 0, `{`}, {itemRightBrace, 1, `}`}},
|
|
|
|
}, {
|
|
|
|
input: "[5m]",
|
|
|
|
expected: []item{
|
|
|
|
{itemLeftBracket, 0, `[`},
|
|
|
|
{itemDuration, 1, `5m`},
|
|
|
|
{itemRightBracket, 3, `]`},
|
|
|
|
},
|
2015-06-02 16:33:49 +00:00
|
|
|
}, {
|
|
|
|
input: "\r\n\r",
|
|
|
|
expected: []item{},
|
2015-03-30 16:12:51 +00:00
|
|
|
},
|
|
|
|
// Test numbers.
|
|
|
|
{
|
|
|
|
input: "1",
|
|
|
|
expected: []item{{itemNumber, 0, "1"}},
|
|
|
|
}, {
|
|
|
|
input: "4.23",
|
|
|
|
expected: []item{{itemNumber, 0, "4.23"}},
|
|
|
|
}, {
|
|
|
|
input: ".3",
|
|
|
|
expected: []item{{itemNumber, 0, ".3"}},
|
|
|
|
}, {
|
|
|
|
input: "5.",
|
|
|
|
expected: []item{{itemNumber, 0, "5."}},
|
|
|
|
}, {
|
|
|
|
input: "NaN",
|
|
|
|
expected: []item{{itemNumber, 0, "NaN"}},
|
|
|
|
}, {
|
|
|
|
input: "nAN",
|
|
|
|
expected: []item{{itemNumber, 0, "nAN"}},
|
|
|
|
}, {
|
|
|
|
input: "NaN 123",
|
|
|
|
expected: []item{{itemNumber, 0, "NaN"}, {itemNumber, 4, "123"}},
|
|
|
|
}, {
|
|
|
|
input: "NaN123",
|
|
|
|
expected: []item{{itemIdentifier, 0, "NaN123"}},
|
|
|
|
}, {
|
|
|
|
input: "iNf",
|
|
|
|
expected: []item{{itemNumber, 0, "iNf"}},
|
|
|
|
}, {
|
|
|
|
input: "Inf",
|
|
|
|
expected: []item{{itemNumber, 0, "Inf"}},
|
|
|
|
}, {
|
|
|
|
input: "+Inf",
|
|
|
|
expected: []item{{itemADD, 0, "+"}, {itemNumber, 1, "Inf"}},
|
|
|
|
}, {
|
|
|
|
input: "+Inf 123",
|
|
|
|
expected: []item{{itemADD, 0, "+"}, {itemNumber, 1, "Inf"}, {itemNumber, 5, "123"}},
|
|
|
|
}, {
|
|
|
|
input: "-Inf",
|
|
|
|
expected: []item{{itemSUB, 0, "-"}, {itemNumber, 1, "Inf"}},
|
|
|
|
}, {
|
|
|
|
input: "Infoo",
|
|
|
|
expected: []item{{itemIdentifier, 0, "Infoo"}},
|
|
|
|
}, {
|
|
|
|
input: "-Infoo",
|
|
|
|
expected: []item{{itemSUB, 0, "-"}, {itemIdentifier, 1, "Infoo"}},
|
|
|
|
}, {
|
|
|
|
input: "-Inf 123",
|
|
|
|
expected: []item{{itemSUB, 0, "-"}, {itemNumber, 1, "Inf"}, {itemNumber, 5, "123"}},
|
|
|
|
}, {
|
|
|
|
input: "0x123",
|
|
|
|
expected: []item{{itemNumber, 0, "0x123"}},
|
|
|
|
},
|
2016-03-01 22:23:18 +00:00
|
|
|
// Test strings.
|
|
|
|
{
|
2016-03-02 00:13:27 +00:00
|
|
|
input: "\"test\\tsequence\"",
|
|
|
|
expected: []item{{itemString, 0, `"test\tsequence"`}},
|
2016-03-01 22:23:18 +00:00
|
|
|
},
|
|
|
|
{
|
|
|
|
input: "\"test\\\\.expression\"",
|
|
|
|
expected: []item{{itemString, 0, `"test\\.expression"`}},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
input: "\"test\\.expression\"",
|
|
|
|
expected: []item{
|
|
|
|
{itemError, 0, "unknown escape sequence U+002E '.'"},
|
|
|
|
{itemString, 0, `"test\.expression"`},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
input: "`test\\.expression`",
|
|
|
|
expected: []item{{itemString, 0, "`test\\.expression`"}},
|
|
|
|
},
|
2015-07-29 00:11:13 +00:00
|
|
|
{
|
|
|
|
// See https://github.com/prometheus/prometheus/issues/939.
|
|
|
|
input: ".٩",
|
|
|
|
fail: true,
|
|
|
|
},
|
2015-03-30 16:12:51 +00:00
|
|
|
// Test duration.
|
|
|
|
{
|
|
|
|
input: "5s",
|
|
|
|
expected: []item{{itemDuration, 0, "5s"}},
|
|
|
|
}, {
|
|
|
|
input: "123m",
|
|
|
|
expected: []item{{itemDuration, 0, "123m"}},
|
|
|
|
}, {
|
|
|
|
input: "1h",
|
|
|
|
expected: []item{{itemDuration, 0, "1h"}},
|
|
|
|
}, {
|
|
|
|
input: "3w",
|
|
|
|
expected: []item{{itemDuration, 0, "3w"}},
|
|
|
|
}, {
|
|
|
|
input: "1y",
|
|
|
|
expected: []item{{itemDuration, 0, "1y"}},
|
|
|
|
},
|
|
|
|
// Test identifiers.
|
|
|
|
{
|
|
|
|
input: "abc",
|
|
|
|
expected: []item{{itemIdentifier, 0, "abc"}},
|
|
|
|
}, {
|
|
|
|
input: "a:bc",
|
|
|
|
expected: []item{{itemMetricIdentifier, 0, "a:bc"}},
|
|
|
|
}, {
|
|
|
|
input: "abc d",
|
|
|
|
expected: []item{{itemIdentifier, 0, "abc"}, {itemIdentifier, 4, "d"}},
|
2015-05-11 09:45:23 +00:00
|
|
|
}, {
|
|
|
|
input: ":bc",
|
|
|
|
expected: []item{{itemMetricIdentifier, 0, ":bc"}},
|
|
|
|
}, {
|
|
|
|
input: "0a:bc",
|
|
|
|
fail: true,
|
2015-03-30 16:12:51 +00:00
|
|
|
},
|
|
|
|
// Test comments.
|
|
|
|
{
|
|
|
|
input: "# some comment",
|
|
|
|
expected: []item{{itemComment, 0, "# some comment"}},
|
|
|
|
}, {
|
|
|
|
input: "5 # 1+1\n5",
|
|
|
|
expected: []item{
|
|
|
|
{itemNumber, 0, "5"},
|
|
|
|
{itemComment, 2, "# 1+1"},
|
|
|
|
{itemNumber, 8, "5"},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
// Test operators.
|
|
|
|
{
|
|
|
|
input: `=`,
|
|
|
|
expected: []item{{itemAssign, 0, `=`}},
|
|
|
|
}, {
|
|
|
|
// Inside braces equality is a single '=' character.
|
|
|
|
input: `{=}`,
|
|
|
|
expected: []item{{itemLeftBrace, 0, `{`}, {itemEQL, 1, `=`}, {itemRightBrace, 2, `}`}},
|
|
|
|
}, {
|
|
|
|
input: `==`,
|
|
|
|
expected: []item{{itemEQL, 0, `==`}},
|
|
|
|
}, {
|
|
|
|
input: `!=`,
|
|
|
|
expected: []item{{itemNEQ, 0, `!=`}},
|
|
|
|
}, {
|
|
|
|
input: `<`,
|
|
|
|
expected: []item{{itemLSS, 0, `<`}},
|
|
|
|
}, {
|
|
|
|
input: `>`,
|
|
|
|
expected: []item{{itemGTR, 0, `>`}},
|
|
|
|
}, {
|
|
|
|
input: `>=`,
|
|
|
|
expected: []item{{itemGTE, 0, `>=`}},
|
|
|
|
}, {
|
|
|
|
input: `<=`,
|
|
|
|
expected: []item{{itemLTE, 0, `<=`}},
|
|
|
|
}, {
|
|
|
|
input: `+`,
|
|
|
|
expected: []item{{itemADD, 0, `+`}},
|
|
|
|
}, {
|
|
|
|
input: `-`,
|
|
|
|
expected: []item{{itemSUB, 0, `-`}},
|
|
|
|
}, {
|
|
|
|
input: `*`,
|
|
|
|
expected: []item{{itemMUL, 0, `*`}},
|
|
|
|
}, {
|
|
|
|
input: `/`,
|
|
|
|
expected: []item{{itemDIV, 0, `/`}},
|
2016-05-29 09:06:14 +00:00
|
|
|
}, {
|
|
|
|
input: `^`,
|
|
|
|
expected: []item{{itemPOW, 0, `^`}},
|
2015-03-30 16:12:51 +00:00
|
|
|
}, {
|
|
|
|
input: `%`,
|
|
|
|
expected: []item{{itemMOD, 0, `%`}},
|
|
|
|
}, {
|
|
|
|
input: `AND`,
|
|
|
|
expected: []item{{itemLAND, 0, `AND`}},
|
|
|
|
}, {
|
|
|
|
input: `or`,
|
|
|
|
expected: []item{{itemLOR, 0, `or`}},
|
2016-04-02 22:52:18 +00:00
|
|
|
}, {
|
|
|
|
input: `unless`,
|
|
|
|
expected: []item{{itemLUnless, 0, `unless`}},
|
2015-03-30 16:12:51 +00:00
|
|
|
},
|
|
|
|
// Test aggregators.
|
|
|
|
{
|
|
|
|
input: `sum`,
|
|
|
|
expected: []item{{itemSum, 0, `sum`}},
|
|
|
|
}, {
|
|
|
|
input: `AVG`,
|
|
|
|
expected: []item{{itemAvg, 0, `AVG`}},
|
|
|
|
}, {
|
|
|
|
input: `MAX`,
|
|
|
|
expected: []item{{itemMax, 0, `MAX`}},
|
|
|
|
}, {
|
|
|
|
input: `min`,
|
|
|
|
expected: []item{{itemMin, 0, `min`}},
|
|
|
|
}, {
|
|
|
|
input: `count`,
|
|
|
|
expected: []item{{itemCount, 0, `count`}},
|
|
|
|
}, {
|
|
|
|
input: `stdvar`,
|
|
|
|
expected: []item{{itemStdvar, 0, `stdvar`}},
|
|
|
|
}, {
|
|
|
|
input: `stddev`,
|
|
|
|
expected: []item{{itemStddev, 0, `stddev`}},
|
|
|
|
},
|
|
|
|
// Test keywords.
|
|
|
|
{
|
|
|
|
input: "alert",
|
|
|
|
expected: []item{{itemAlert, 0, "alert"}},
|
2015-06-12 12:21:01 +00:00
|
|
|
}, {
|
|
|
|
input: "keep_common",
|
|
|
|
expected: []item{{itemKeepCommon, 0, "keep_common"}},
|
2015-03-30 16:12:51 +00:00
|
|
|
}, {
|
|
|
|
input: "if",
|
|
|
|
expected: []item{{itemIf, 0, "if"}},
|
|
|
|
}, {
|
|
|
|
input: "for",
|
|
|
|
expected: []item{{itemFor, 0, "for"}},
|
|
|
|
}, {
|
2015-12-23 13:54:02 +00:00
|
|
|
input: "labels",
|
|
|
|
expected: []item{{itemLabels, 0, "labels"}},
|
2015-03-30 16:12:51 +00:00
|
|
|
}, {
|
2015-12-11 16:02:34 +00:00
|
|
|
input: "annotations",
|
|
|
|
expected: []item{{itemAnnotations, 0, "annotations"}},
|
2015-03-30 16:12:51 +00:00
|
|
|
}, {
|
|
|
|
input: "offset",
|
|
|
|
expected: []item{{itemOffset, 0, "offset"}},
|
|
|
|
}, {
|
|
|
|
input: "by",
|
|
|
|
expected: []item{{itemBy, 0, "by"}},
|
2016-02-07 18:03:16 +00:00
|
|
|
}, {
|
|
|
|
input: "without",
|
|
|
|
expected: []item{{itemWithout, 0, "without"}},
|
2015-03-30 16:12:51 +00:00
|
|
|
}, {
|
|
|
|
input: "on",
|
|
|
|
expected: []item{{itemOn, 0, "on"}},
|
2016-04-21 10:45:06 +00:00
|
|
|
}, {
|
|
|
|
input: "ignoring",
|
|
|
|
expected: []item{{itemIgnoring, 0, "ignoring"}},
|
2015-03-30 16:12:51 +00:00
|
|
|
}, {
|
|
|
|
input: "group_left",
|
|
|
|
expected: []item{{itemGroupLeft, 0, "group_left"}},
|
|
|
|
}, {
|
|
|
|
input: "group_right",
|
|
|
|
expected: []item{{itemGroupRight, 0, "group_right"}},
|
2015-09-02 13:51:44 +00:00
|
|
|
}, {
|
|
|
|
input: "bool",
|
|
|
|
expected: []item{{itemBool, 0, "bool"}},
|
2015-03-30 16:12:51 +00:00
|
|
|
},
|
|
|
|
// Test Selector.
|
|
|
|
{
|
2015-04-29 14:35:18 +00:00
|
|
|
input: `台北`,
|
|
|
|
fail: true,
|
2015-05-11 09:45:23 +00:00
|
|
|
}, {
|
|
|
|
input: `{台北='a'}`,
|
|
|
|
fail: true,
|
|
|
|
}, {
|
|
|
|
input: `{0a='a'}`,
|
|
|
|
fail: true,
|
2015-05-08 14:43:02 +00:00
|
|
|
}, {
|
|
|
|
input: `{foo='bar'}`,
|
|
|
|
expected: []item{
|
|
|
|
{itemLeftBrace, 0, `{`},
|
|
|
|
{itemIdentifier, 1, `foo`},
|
|
|
|
{itemEQL, 4, `=`},
|
|
|
|
{itemString, 5, `'bar'`},
|
|
|
|
{itemRightBrace, 10, `}`},
|
|
|
|
},
|
2015-04-29 14:35:18 +00:00
|
|
|
}, {
|
2015-03-30 16:12:51 +00:00
|
|
|
input: `{foo="bar"}`,
|
|
|
|
expected: []item{
|
|
|
|
{itemLeftBrace, 0, `{`},
|
|
|
|
{itemIdentifier, 1, `foo`},
|
|
|
|
{itemEQL, 4, `=`},
|
|
|
|
{itemString, 5, `"bar"`},
|
|
|
|
{itemRightBrace, 10, `}`},
|
|
|
|
},
|
2015-05-08 14:43:02 +00:00
|
|
|
}, {
|
|
|
|
input: `{foo="bar\"bar"}`,
|
|
|
|
expected: []item{
|
|
|
|
{itemLeftBrace, 0, `{`},
|
|
|
|
{itemIdentifier, 1, `foo`},
|
|
|
|
{itemEQL, 4, `=`},
|
|
|
|
{itemString, 5, `"bar\"bar"`},
|
|
|
|
{itemRightBrace, 15, `}`},
|
|
|
|
},
|
2015-03-30 16:12:51 +00:00
|
|
|
}, {
|
|
|
|
input: `{NaN != "bar" }`,
|
|
|
|
expected: []item{
|
|
|
|
{itemLeftBrace, 0, `{`},
|
|
|
|
{itemIdentifier, 1, `NaN`},
|
|
|
|
{itemNEQ, 5, `!=`},
|
|
|
|
{itemString, 8, `"bar"`},
|
|
|
|
{itemRightBrace, 14, `}`},
|
|
|
|
},
|
|
|
|
}, {
|
|
|
|
input: `{alert=~"bar" }`,
|
|
|
|
expected: []item{
|
|
|
|
{itemLeftBrace, 0, `{`},
|
|
|
|
{itemIdentifier, 1, `alert`},
|
|
|
|
{itemEQLRegex, 6, `=~`},
|
|
|
|
{itemString, 8, `"bar"`},
|
|
|
|
{itemRightBrace, 14, `}`},
|
|
|
|
},
|
|
|
|
}, {
|
|
|
|
input: `{on!~"bar"}`,
|
|
|
|
expected: []item{
|
|
|
|
{itemLeftBrace, 0, `{`},
|
|
|
|
{itemIdentifier, 1, `on`},
|
|
|
|
{itemNEQRegex, 3, `!~`},
|
|
|
|
{itemString, 5, `"bar"`},
|
|
|
|
{itemRightBrace, 10, `}`},
|
|
|
|
},
|
|
|
|
}, {
|
|
|
|
input: `{alert!#"bar"}`, fail: true,
|
|
|
|
}, {
|
|
|
|
input: `{foo:a="bar"}`, fail: true,
|
|
|
|
},
|
|
|
|
// Test common errors.
|
|
|
|
{
|
|
|
|
input: `=~`, fail: true,
|
|
|
|
}, {
|
|
|
|
input: `!~`, fail: true,
|
|
|
|
}, {
|
|
|
|
input: `!(`, fail: true,
|
|
|
|
}, {
|
|
|
|
input: "1a", fail: true,
|
|
|
|
},
|
|
|
|
// Test mismatched parens.
|
|
|
|
{
|
|
|
|
input: `(`, fail: true,
|
|
|
|
}, {
|
|
|
|
input: `())`, fail: true,
|
|
|
|
}, {
|
|
|
|
input: `(()`, fail: true,
|
|
|
|
}, {
|
|
|
|
input: `{`, fail: true,
|
|
|
|
}, {
|
|
|
|
input: `}`, fail: true,
|
|
|
|
}, {
|
|
|
|
input: "{{", fail: true,
|
|
|
|
}, {
|
|
|
|
input: "{{}}", fail: true,
|
|
|
|
}, {
|
|
|
|
input: `[`, fail: true,
|
|
|
|
}, {
|
|
|
|
input: `[[`, fail: true,
|
|
|
|
}, {
|
|
|
|
input: `[]]`, fail: true,
|
|
|
|
}, {
|
|
|
|
input: `[[]]`, fail: true,
|
|
|
|
}, {
|
|
|
|
input: `]`, fail: true,
|
|
|
|
},
|
2015-05-12 08:39:10 +00:00
|
|
|
// Test series description.
|
|
|
|
{
|
|
|
|
input: `{} _ 1 x .3`,
|
|
|
|
expected: []item{
|
|
|
|
{itemLeftBrace, 0, `{`},
|
|
|
|
{itemRightBrace, 1, `}`},
|
|
|
|
{itemBlank, 3, `_`},
|
|
|
|
{itemNumber, 5, `1`},
|
|
|
|
{itemTimes, 7, `x`},
|
|
|
|
{itemNumber, 9, `.3`},
|
|
|
|
},
|
|
|
|
seriesDesc: true,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
input: `metric +Inf Inf NaN`,
|
|
|
|
expected: []item{
|
|
|
|
{itemIdentifier, 0, `metric`},
|
|
|
|
{itemADD, 7, `+`},
|
|
|
|
{itemNumber, 8, `Inf`},
|
|
|
|
{itemNumber, 12, `Inf`},
|
|
|
|
{itemNumber, 16, `NaN`},
|
|
|
|
},
|
|
|
|
seriesDesc: true,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
input: `metric 1+1x4`,
|
|
|
|
expected: []item{
|
|
|
|
{itemIdentifier, 0, `metric`},
|
|
|
|
{itemNumber, 7, `1`},
|
|
|
|
{itemADD, 8, `+`},
|
|
|
|
{itemNumber, 9, `1`},
|
|
|
|
{itemTimes, 10, `x`},
|
|
|
|
{itemNumber, 11, `4`},
|
|
|
|
},
|
|
|
|
seriesDesc: true,
|
|
|
|
},
|
2015-03-30 16:12:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// TestLexer tests basic functionality of the lexer. More elaborate tests are implemented
|
|
|
|
// for the parser to avoid duplicated effort.
|
|
|
|
func TestLexer(t *testing.T) {
|
|
|
|
for i, test := range tests {
|
2016-08-29 07:20:43 +00:00
|
|
|
l := &lexer{
|
|
|
|
input: test.input,
|
|
|
|
items: make(chan item),
|
|
|
|
seriesDesc: test.seriesDesc,
|
|
|
|
}
|
|
|
|
go l.run()
|
|
|
|
|
2015-03-30 16:12:51 +00:00
|
|
|
out := []item{}
|
|
|
|
for it := range l.items {
|
|
|
|
out = append(out, it)
|
|
|
|
}
|
|
|
|
|
|
|
|
lastItem := out[len(out)-1]
|
|
|
|
if test.fail {
|
|
|
|
if lastItem.typ != itemError {
|
2015-05-12 08:39:10 +00:00
|
|
|
t.Logf("%d: input %q", i, test.input)
|
|
|
|
t.Fatalf("expected lexing error but did not fail")
|
2015-03-30 16:12:51 +00:00
|
|
|
}
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if lastItem.typ == itemError {
|
2015-05-12 08:39:10 +00:00
|
|
|
t.Logf("%d: input %q", i, test.input)
|
|
|
|
t.Fatalf("unexpected lexing error at position %d: %s", lastItem.pos, lastItem)
|
2015-03-30 16:12:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if !reflect.DeepEqual(lastItem, item{itemEOF, Pos(len(test.input)), ""}) {
|
2015-05-12 08:39:10 +00:00
|
|
|
t.Logf("%d: input %q", i, test.input)
|
|
|
|
t.Fatalf("lexing error: expected output to end with EOF item.\ngot:\n%s", expectedList(out))
|
2015-03-30 16:12:51 +00:00
|
|
|
}
|
|
|
|
out = out[:len(out)-1]
|
|
|
|
if !reflect.DeepEqual(out, test.expected) {
|
2015-05-12 08:39:10 +00:00
|
|
|
t.Logf("%d: input %q", i, test.input)
|
|
|
|
t.Fatalf("lexing mismatch:\nexpected:\n%s\ngot:\n%s", expectedList(test.expected), expectedList(out))
|
2015-03-30 16:12:51 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2015-05-12 08:39:10 +00:00
|
|
|
|
|
|
|
func expectedList(exp []item) string {
|
|
|
|
s := ""
|
|
|
|
for _, it := range exp {
|
|
|
|
s += fmt.Sprintf("\t%#v\n", it)
|
|
|
|
}
|
|
|
|
return s
|
|
|
|
}
|