increased test coverage (83.9%)
This commit is contained in:
+11
-13
@@ -42,12 +42,16 @@ func ImportInContextByGlobPattern(ctx kern.ExprContext, pattern string) (count i
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func fixCtrlVar(name string) string {
|
||||||
|
if !strings.HasPrefix(name, "_") {
|
||||||
|
name = "_" + name
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
func GlobalCtrlSet(ctx kern.ExprContext, name string, newValue any) (currentValue any) {
|
func GlobalCtrlSet(ctx kern.ExprContext, name string, newValue any) (currentValue any) {
|
||||||
if globalCtx := ctx.GetGlobal(); globalCtx != nil {
|
if globalCtx := ctx.GetGlobal(); globalCtx != nil {
|
||||||
if !strings.HasPrefix(name, "_") {
|
name = fixCtrlVar(name)
|
||||||
name = "_" + name
|
|
||||||
}
|
|
||||||
|
|
||||||
currentValue, _ = globalCtx.GetVar(name)
|
currentValue, _ = globalCtx.GetVar(name)
|
||||||
globalCtx.SetVar(name, newValue)
|
globalCtx.SetVar(name, newValue)
|
||||||
}
|
}
|
||||||
@@ -56,18 +60,14 @@ func GlobalCtrlSet(ctx kern.ExprContext, name string, newValue any) (currentValu
|
|||||||
|
|
||||||
func GlobalCtrlGet(ctx kern.ExprContext, name string) (currentValue any) {
|
func GlobalCtrlGet(ctx kern.ExprContext, name string) (currentValue any) {
|
||||||
if globalCtx := ctx.GetGlobal(); globalCtx != nil {
|
if globalCtx := ctx.GetGlobal(); globalCtx != nil {
|
||||||
if !strings.HasPrefix(name, "_") {
|
name = fixCtrlVar(name)
|
||||||
name = "_" + name
|
|
||||||
}
|
|
||||||
currentValue, _ = globalCtx.GetVar(name)
|
currentValue, _ = globalCtx.GetVar(name)
|
||||||
}
|
}
|
||||||
return currentValue
|
return currentValue
|
||||||
}
|
}
|
||||||
|
|
||||||
func CtrlEnable(ctx kern.ExprContext, name string) (currentStatus bool) {
|
func CtrlEnable(ctx kern.ExprContext, name string) (currentStatus bool) {
|
||||||
if !strings.HasPrefix(name, "_") {
|
name = fixCtrlVar(name)
|
||||||
name = "_" + name
|
|
||||||
}
|
|
||||||
if v, exists := ctx.GetVar(name); exists && kern.IsBool(v) {
|
if v, exists := ctx.GetVar(name); exists && kern.IsBool(v) {
|
||||||
currentStatus, _ = v.(bool)
|
currentStatus, _ = v.(bool)
|
||||||
}
|
}
|
||||||
@@ -77,9 +77,7 @@ func CtrlEnable(ctx kern.ExprContext, name string) (currentStatus bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func CtrlDisable(ctx kern.ExprContext, name string) (currentStatus bool) {
|
func CtrlDisable(ctx kern.ExprContext, name string) (currentStatus bool) {
|
||||||
if !strings.HasPrefix(name, "_") {
|
name = fixCtrlVar(name)
|
||||||
name = "_" + name
|
|
||||||
}
|
|
||||||
if v, exists := ctx.GetVar(name); exists && kern.IsBool(v) {
|
if v, exists := ctx.GetVar(name); exists && kern.IsBool(v) {
|
||||||
currentStatus, _ = v.(bool)
|
currentStatus, _ = v.(bool)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ type IntIterator struct {
|
|||||||
step int64
|
step int64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewIntIteratorA(args ...any) (it *IntIterator, err error) {
|
||||||
|
return NewIntIterator(args)
|
||||||
|
}
|
||||||
|
|
||||||
func NewIntIterator(args []any) (it *IntIterator, err error) {
|
func NewIntIterator(args []any) (it *IntIterator, err error) {
|
||||||
var argc int = 0
|
var argc int = 0
|
||||||
if args != nil {
|
if args != nil {
|
||||||
|
|||||||
+6
-6
@@ -38,9 +38,9 @@ func HasIterStandardOperations(name string) bool {
|
|||||||
return slices.Contains([]string{kern.NextName, kern.ResetName, kern.IndexName, kern.CountName, kern.CurrentName, kern.CleanName}, name)
|
return slices.Contains([]string{kern.NextName, kern.ResetName, kern.IndexName, kern.CountName, kern.CurrentName, kern.CleanName}, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func HasIterOperations(name string, ops ...string) bool {
|
// func HasIterOperations(name string, ops ...string) bool {
|
||||||
return slices.Contains([]string{
|
// return slices.Contains([]string{
|
||||||
kern.NextName, kern.ResetName, kern.IndexName, kern.CountName, kern.CurrentName, kern.CleanName,
|
// kern.NextName, kern.ResetName, kern.IndexName, kern.CountName, kern.CurrentName, kern.CleanName,
|
||||||
}, name) ||
|
// }, name) ||
|
||||||
slices.Contains(ops, name)
|
// slices.Contains(ops, name)
|
||||||
}
|
// }
|
||||||
|
|||||||
+23
-21
@@ -40,23 +40,7 @@ func evalContextValue(ctx kern.ExprContext, opTerm *scan.Term) (v any, err error
|
|||||||
}
|
}
|
||||||
|
|
||||||
if sourceCtx != nil {
|
if sourceCtx != nil {
|
||||||
if formatter, ok := sourceCtx.(kern.DictFormat); ok {
|
v = contextToDict(sourceCtx)
|
||||||
v = formatter.ToDict()
|
|
||||||
} else if formatter, ok := sourceCtx.(kern.Formatter); ok {
|
|
||||||
v = formatter.ToString(0)
|
|
||||||
} else {
|
|
||||||
// keys := sourceCtx.EnumVars(func(name string) bool { return name[0] != '_' })
|
|
||||||
keys := sourceCtx.EnumVars(nil)
|
|
||||||
d := make(map[string]any)
|
|
||||||
for _, key := range keys {
|
|
||||||
d[key], _ = sourceCtx.GetVar(key)
|
|
||||||
}
|
|
||||||
keys = sourceCtx.EnumFuncs(func(name string) bool { return true })
|
|
||||||
for _, key := range keys {
|
|
||||||
d[key], _ = sourceCtx.GetFuncInfo(key)
|
|
||||||
}
|
|
||||||
v = d
|
|
||||||
}
|
|
||||||
} else if childValue != nil {
|
} else if childValue != nil {
|
||||||
it, ok := childValue.(kern.Iterator)
|
it, ok := childValue.(kern.Iterator)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -64,18 +48,14 @@ func evalContextValue(ctx kern.ExprContext, opTerm *scan.Term) (v any, err error
|
|||||||
}
|
}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
var item any
|
var item any
|
||||||
// values := kern.NewListA()
|
|
||||||
values := kern.NewLinkedListA()
|
values := kern.NewLinkedListA()
|
||||||
for item, err = it.Next(); err == nil; item, err = it.Next() {
|
for item, err = it.Next(); err == nil; item, err = it.Next() {
|
||||||
// values.AppendItem(item)
|
|
||||||
values.PushBack(item)
|
values.PushBack(item)
|
||||||
}
|
}
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
err = nil
|
err = nil
|
||||||
v = values
|
v = values
|
||||||
}
|
}
|
||||||
// } else {
|
|
||||||
// err = opTerm.ErrIncompatiblePrefixPostfixType(childValue)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
err = opTerm.ErrIncompatiblePrefixPostfixType(childValue)
|
err = opTerm.ErrIncompatiblePrefixPostfixType(childValue)
|
||||||
@@ -83,6 +63,28 @@ func evalContextValue(ctx kern.ExprContext, opTerm *scan.Term) (v any, err error
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func contextToDict(ctx kern.ExprContext) (dict *kern.DictType) {
|
||||||
|
// Variables
|
||||||
|
keys := ctx.EnumVars(nil)
|
||||||
|
vars := kern.MakeDict()
|
||||||
|
for _, key := range keys {
|
||||||
|
value, _ := ctx.GetVar(key)
|
||||||
|
vars.SetItem(key, value)
|
||||||
|
}
|
||||||
|
// Functions
|
||||||
|
keys = ctx.EnumFuncs(func(name string) bool { return true })
|
||||||
|
funcs := kern.MakeDict()
|
||||||
|
for _, key := range keys {
|
||||||
|
funcInfo, _ := ctx.GetFuncInfo(key)
|
||||||
|
funcs.SetItem(key, funcInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
dict = kern.MakeDict()
|
||||||
|
dict.SetItem("vars", vars)
|
||||||
|
dict.SetItem("funcs", funcs)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// init
|
// init
|
||||||
func init() {
|
func init() {
|
||||||
scan.RegisterTermConstructor(scan.SymDoubleDollar, newContextTerm)
|
scan.RegisterTermConstructor(scan.SymDoubleDollar, newContextTerm)
|
||||||
|
|||||||
@@ -121,3 +121,27 @@ func logTest(t *testing.T, n int, section, source string, wantResult any, wantEr
|
|||||||
t.Logf("[-]%s nr %3d -- `%s` --> %v", section, n, source, wantErr)
|
t.Logf("[-]%s nr %3d -- `%s` --> %v", section, n, source, wantErr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testIteratorCallOp(t *testing.T, section string, it kern.Iterator) {
|
||||||
|
if _, err := it.CallOperation("next", map[string]any{}); err != nil {
|
||||||
|
t.Errorf(`%s -- CallOperation("next") failed: %v`, section, err)
|
||||||
|
}
|
||||||
|
if _, err := it.CallOperation("current", map[string]any{}); err != nil {
|
||||||
|
t.Errorf(`%s -- CallOperation("current") failed: %v`, section, err)
|
||||||
|
}
|
||||||
|
if _, err := it.CallOperation("clean", map[string]any{}); err != nil {
|
||||||
|
t.Errorf(`%s -- CallOperation("clean") failed: %v`, section, err)
|
||||||
|
}
|
||||||
|
if _, err := it.CallOperation("index", map[string]any{}); err != nil {
|
||||||
|
t.Errorf(`%s -- CallOperation("index") failed: %v`, section, err)
|
||||||
|
}
|
||||||
|
if _, err := it.CallOperation("count", map[string]any{}); err != nil {
|
||||||
|
t.Errorf(`%s -- CallOperation("count") failed: %v`, section, err)
|
||||||
|
}
|
||||||
|
if _, err := it.CallOperation("reset", map[string]any{}); err != nil {
|
||||||
|
t.Errorf(`%s -- CallOperation("reset") failed: %v`, section, err)
|
||||||
|
}
|
||||||
|
if _, err := it.CallOperation("fake", map[string]any{}); err == nil {
|
||||||
|
t.Errorf(`%s -- CallOperation("fake") should return error`, section)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ package expr
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"git.portale-stac.it/go-pkg/expr/kern"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCtrlSet(t *testing.T) {
|
func TestCtrlSet(t *testing.T) {
|
||||||
@@ -58,3 +60,35 @@ func TestCtrlGetNotDefined(t *testing.T) {
|
|||||||
t.Errorf(`%s -- CtrlGet(%q) should have returned nil, got %v`, section, varName, v)
|
t.Errorf(`%s -- CtrlGet(%q) should have returned nil, got %v`, section, varName, v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCtrlEnable(t *testing.T) {
|
||||||
|
section := "Context"
|
||||||
|
varName := "test_var"
|
||||||
|
varValue := true
|
||||||
|
|
||||||
|
ctx := NewSimpleStore()
|
||||||
|
GlobalCtrlSet(ctx, varName, varValue)
|
||||||
|
if !CtrlEnable(ctx, varName) {
|
||||||
|
t.Errorf(`%s -- CtrlEnable(ctx, %q) should have returned 'true', got 'false'`, section, varName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !CtrlDisable(ctx, varName) {
|
||||||
|
t.Errorf(`%s -- CtrlEnable(ctx, %q) should have returned 'true', got 'false'`, section, varName)
|
||||||
|
// t.Errorf(`%s -- CtrlEnable(ctx, %q) should have returned 'false', got 'true'`, section, varName)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestList(t *testing.T) {
|
||||||
|
section := "Context"
|
||||||
|
|
||||||
|
inputs := []inputType{
|
||||||
|
/* 1 */ {`$$(5)`, kern.NewLinkedListA(5), nil},
|
||||||
|
/* 2 */ {`$$($(2))`, kern.NewLinkedListA(0, 1), nil},
|
||||||
|
/* 3 */ {`string(($$global).funcs.bool)`, `bool(value):boolean{}`, nil},
|
||||||
|
}
|
||||||
|
|
||||||
|
runTestSuiteSpec(t, section, inputs, 3)
|
||||||
|
// runTestSuite(t, section, inputs)
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
+2
-2
@@ -47,8 +47,8 @@ func TestDictParser(t *testing.T) {
|
|||||||
/* 24 */ {`f=func(n){"x"+n}; d={f(5) but "z":10}`, kern.NewDict(map[any]any{"z": int64(10)}), nil},
|
/* 24 */ {`f=func(n){"x"+n}; d={f(5) but "z":10}`, kern.NewDict(map[any]any{"z": int64(10)}), nil},
|
||||||
}
|
}
|
||||||
|
|
||||||
runTestSuiteSpec(t, section, inputs, 1, 23, 24)
|
// runTestSuiteSpec(t, section, inputs, 23, 24)
|
||||||
// runTestSuite(t, section, inputs)
|
runTestSuite(t, section, inputs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAccessSubFields(t *testing.T) {
|
func TestAccessSubFields(t *testing.T) {
|
||||||
|
|||||||
+1
-2
@@ -31,8 +31,7 @@ func TestExpr(t *testing.T) {
|
|||||||
/* 15 */ {`a=3; a*=2)+1; a`, nil, `[1:11] unexpected token ")"`},
|
/* 15 */ {`a=3; a*=2)+1; a`, nil, `[1:11] unexpected token ")"`},
|
||||||
/* 16 */ {`v=[2]; a=1; v[a-=1]=5; v[0]`, int64(5), nil},
|
/* 16 */ {`v=[2]; a=1; v[a-=1]=5; v[0]`, int64(5), nil},
|
||||||
/* 17 */ {`true ? {"a"} :: {"b"}`, "a", nil},
|
/* 17 */ {`true ? {"a"} :: {"b"}`, "a", nil},
|
||||||
/* 18 */ {`$$`, kern.NewDict(map[any]any{"variables": kern.NewDict(nil), "functions": kern.NewDict(nil)}), nil},
|
/* 18 */ {`$$`, kern.NewDict(map[any]any{"vars": kern.NewDict(nil), "funcs": kern.NewDict(nil)}), nil},
|
||||||
///* 19 */ {`$$global`, NewDict(map[any]any{"variables": NewDict(nil), "functions": NewDict(nil)}), nil},
|
|
||||||
/* 19 */ {`
|
/* 19 */ {`
|
||||||
ds={
|
ds={
|
||||||
"init":func(@end){@current=0 but true},
|
"init":func(@end){@current=0 but true},
|
||||||
|
|||||||
+79
-1
@@ -8,6 +8,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.portale-stac.it/go-pkg/expr/kern"
|
"git.portale-stac.it/go-pkg/expr/kern"
|
||||||
|
"git.portale-stac.it/go-pkg/expr/scan"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestIteratorParser(t *testing.T) {
|
func TestIteratorParser(t *testing.T) {
|
||||||
@@ -35,12 +36,89 @@ func TestIteratorParser(t *testing.T) {
|
|||||||
/* 20 */ {`it=$({1:"one",2:"two",3:"three"}, "default", "value"); it++`, "one", nil},
|
/* 20 */ {`it=$({1:"one",2:"two",3:"three"}, "default", "value"); it++`, "one", nil},
|
||||||
/* 21 */ {`it=$({1:"one",2:"two",3:"three"}, "desc", "key"); it++`, int64(3), nil},
|
/* 21 */ {`it=$({1:"one",2:"two",3:"three"}, "desc", "key"); it++`, int64(3), nil},
|
||||||
/* 22 */ {`it=$({1:"one",2:"two",3:"three"}, "asc", "item"); it++`, kern.NewList([]any{int64(1), "one"}), nil},
|
/* 22 */ {`it=$({1:"one",2:"two",3:"three"}, "asc", "item"); it++`, kern.NewList([]any{int64(1), "one"}), nil},
|
||||||
|
/* 23 */ {`$$($(1,4,0))`, nil, `step cannot be zero`},
|
||||||
|
/* 24 */ {`$$($(1,4,-1))`, nil, `step cannot be negative when start < stop`},
|
||||||
|
/* 25 */ {`$$($(4,1,1))`, nil, `step cannot be positive when start > stop`},
|
||||||
}
|
}
|
||||||
|
|
||||||
// runTestSuiteSpec(t, section, inputs, 1)
|
// runTestSuiteSpec(t, section, inputs, 25)
|
||||||
runTestSuite(t, section, inputs)
|
runTestSuite(t, section, inputs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCallOpIntIter(t *testing.T) {
|
||||||
|
section := "IntIterator-CallOp"
|
||||||
|
|
||||||
|
if it, err := NewIntIteratorA(int64(1), int64(4), int64(1)); err != nil {
|
||||||
|
t.Errorf(`%s -- NewIntIteratorA() failed: %v`, section, err)
|
||||||
|
} else {
|
||||||
|
testIteratorCallOp(t, section, it)
|
||||||
|
testIterAttrs(t, section, it, "IntIterator", "$(1..4..1)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCallOpIterIter(t *testing.T) {
|
||||||
|
section := "IterIterator-CallOp"
|
||||||
|
|
||||||
|
ctx := NewSimpleStore()
|
||||||
|
if inner, err := NewIntIteratorA(int64(1), int64(4), int64(1)); err == nil {
|
||||||
|
if it, err := NewIterIter(inner, ctx, []*scan.Term{}); err != nil {
|
||||||
|
t.Errorf(`%s -- NewIterIter() failed: %v`, section, err)
|
||||||
|
} else {
|
||||||
|
testIteratorCallOp(t, section, it)
|
||||||
|
testIterAttrs(t, section, it, "IterIter", "$($(1..4..1))")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
t.Errorf(`%s -- can't create inner iterator: %v`, section, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCallOpDictIter(t *testing.T) {
|
||||||
|
section := "DictIterator-CallOp"
|
||||||
|
|
||||||
|
inner := kern.NewDict(map[any]any{"a": 1})
|
||||||
|
if it, err := NewDictIterator(inner, nil); err != nil {
|
||||||
|
t.Errorf(`%s -- NewIterIter() failed: %v`, section, err)
|
||||||
|
} else {
|
||||||
|
testIteratorCallOp(t, section, it)
|
||||||
|
testIterAttrs(t, section, it, "DictIterator", "$({#1})")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCallOpLinkedListIter(t *testing.T) {
|
||||||
|
section := "LinkedListIterator-CallOp"
|
||||||
|
|
||||||
|
inner := kern.NewLinkedListA(1, 2, 3)
|
||||||
|
it := NewLinkedListIterator(inner, nil)
|
||||||
|
testIteratorCallOp(t, section, it)
|
||||||
|
|
||||||
|
// wanted := "$([<#3>])"
|
||||||
|
// got := it.String()
|
||||||
|
// if wanted != got {
|
||||||
|
// t.Errorf(`%s -- LinkedListIterator.String() failed: expected %q, got %q`, section, wanted, got)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// wanted = "LinkedListIterator"
|
||||||
|
// got = it.TypeName()
|
||||||
|
// if wanted != got {
|
||||||
|
// t.Errorf(`%s -- LinkedListIterator.TypeName() failed: expected %q, got %q`, section, wanted, got)
|
||||||
|
// }
|
||||||
|
testIterAttrs(t, section, it, "LinkedListIterator", "$([<#3>])")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testIterAttrs(t *testing.T, section string, it kern.Iterator, name, repr string) {
|
||||||
|
wanted := repr
|
||||||
|
got := it.String()
|
||||||
|
if wanted != got {
|
||||||
|
t.Errorf(`%s -- %s.String() failed: expected %q, got %q`, section, name, wanted, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
wanted = name
|
||||||
|
got = it.TypeName()
|
||||||
|
if wanted != got {
|
||||||
|
t.Errorf(`%s -- %s.TypeName() failed: expected %q, got %q`, section, name, wanted, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestFilterIterator(t *testing.T) {
|
func TestFilterIterator(t *testing.T) {
|
||||||
section := "Iterator-Filter"
|
section := "Iterator-Filter"
|
||||||
inputs := []inputType{
|
inputs := []inputType{
|
||||||
|
|||||||
@@ -35,6 +35,12 @@ func TestOperator(t *testing.T) {
|
|||||||
/* 20 */ {`a=1; a^=2`, int64(3), nil},
|
/* 20 */ {`a=1; a^=2`, int64(3), nil},
|
||||||
/* 21 */ {`a=1; ++a`, int64(2), nil},
|
/* 21 */ {`a=1; ++a`, int64(2), nil},
|
||||||
/* 22 */ {`a=1; --a`, int64(0), nil},
|
/* 22 */ {`a=1; --a`, int64(0), nil},
|
||||||
|
/* 23 */ {`a=1; a++`, int64(1), nil},
|
||||||
|
/* 24 */ {`a=1; a--`, int64(1), nil},
|
||||||
|
/* 25 */ {`a="1"; ++a`, nil, `[1:9] prefix/postfix operator "++" does not support operand '1' [string]`},
|
||||||
|
/* 26 */ {`a="1"; --a`, nil, `[1:9] prefix/postfix operator "--" does not support operand '1' [string]`},
|
||||||
|
/* 27 */ {`a="1"; a++`, nil, `[1:10] prefix/postfix operator "++" does not support operand '1' [string]`},
|
||||||
|
/* 28 */ {`a="1"; a--`, nil, `[1:10] prefix/postfix operator "--" does not support operand '1' [string]`},
|
||||||
}
|
}
|
||||||
|
|
||||||
// t.Setenv("EXPR_PATH", ".")
|
// t.Setenv("EXPR_PATH", ".")
|
||||||
|
|||||||
Reference in New Issue
Block a user