Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
G
Geth-Modification
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
张蕾
Geth-Modification
Commits
00878e5b
Commit
00878e5b
authored
10 years ago
by
Taylor Gerring
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Convert trie tests to gocheck
parent
1d866b5e
master
v1.10.12
v1.10.12-modified
No related merge requests found
Changes
2
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
119 additions
and
164 deletions
+119
-164
main_test.go
trie/main_test.go
+8
-0
trie_test.go
trie/trie_test.go
+111
-164
No files found.
trie/main_test.go
0 → 100644
View file @
00878e5b
package
trie
import
(
checker
"gopkg.in/check.v1"
"testing"
)
func
Test
(
t
*
testing
.
T
)
{
checker
.
TestingT
(
t
)
}
This diff is collapsed.
Click to expand it.
trie/trie_test.go
View file @
00878e5b
package
trie
package
trie
import
(
import
(
"bytes"
"encoding/hex"
"encoding/hex"
"encoding/json"
"encoding/json"
"fmt"
"fmt"
checker
"gopkg.in/check.v1"
"io/ioutil"
"io/ioutil"
"math/rand"
"math/rand"
"net/http"
"net/http"
"reflect"
"testing"
"time"
"time"
"github.com/ethereum/go-ethereum/ethutil"
"github.com/ethereum/go-ethereum/ethutil"
...
@@ -17,6 +15,11 @@ import (
...
@@ -17,6 +15,11 @@ import (
const
LONG_WORD
=
"1234567890abcdefghijklmnopqrstuvwxxzABCEFGHIJKLMNOPQRSTUVWXYZ"
const
LONG_WORD
=
"1234567890abcdefghijklmnopqrstuvwxxzABCEFGHIJKLMNOPQRSTUVWXYZ"
type
TrieSuite
struct
{
db
*
MemDatabase
trie
*
Trie
}
type
MemDatabase
struct
{
type
MemDatabase
struct
{
db
map
[
string
][]
byte
db
map
[
string
][]
byte
}
}
...
@@ -44,140 +47,97 @@ func NewTrie() (*MemDatabase, *Trie) {
...
@@ -44,140 +47,97 @@ func NewTrie() (*MemDatabase, *Trie) {
return
db
,
New
(
db
,
""
)
return
db
,
New
(
db
,
""
)
}
}
func
TestTrieSync
(
t
*
testing
.
T
)
{
func
(
s
*
TrieSuite
)
SetUpTest
(
c
*
checker
.
C
)
{
db
,
trie
:=
NewTrie
()
s
.
db
,
s
.
trie
=
NewTrie
()
trie
.
Update
(
"dog"
,
LONG_WORD
)
if
len
(
db
.
db
)
!=
0
{
t
.
Error
(
"Expected no data in database"
)
}
trie
.
Sync
()
if
len
(
db
.
db
)
==
0
{
t
.
Error
(
"Expected data to be persisted"
)
}
}
}
func
TestTrieDirtyTracking
(
t
*
testing
.
T
)
{
func
(
s
*
TrieSuite
)
TestTrieSync
(
c
*
checker
.
C
)
{
_
,
trie
:=
NewTrie
(
)
s
.
trie
.
Update
(
"dog"
,
LONG_WORD
)
trie
.
Update
(
"dog"
,
LONG_WORD
)
c
.
Assert
(
s
.
db
.
db
,
checker
.
HasLen
,
0
,
checker
.
Commentf
(
"Expected no data in database"
)
)
if
!
trie
.
cache
.
IsDirty
{
s
.
trie
.
Sync
()
t
.
Error
(
"Expected trie to be dirty"
)
c
.
Assert
(
s
.
db
.
db
,
checker
.
HasLen
,
3
)
}
}
trie
.
Sync
()
func
(
s
*
TrieSuite
)
TestTrieDirtyTracking
(
c
*
checker
.
C
)
{
if
trie
.
cache
.
IsDirty
{
s
.
trie
.
Update
(
"dog"
,
LONG_WORD
)
t
.
Error
(
"Expected trie not to be dirty"
)
c
.
Assert
(
s
.
trie
.
cache
.
IsDirty
,
checker
.
Equals
,
true
,
checker
.
Commentf
(
"Expected no data in database"
))
}
trie
.
Update
(
"test"
,
LONG_WORD
)
s
.
trie
.
Sync
()
trie
.
cache
.
Undo
()
c
.
Assert
(
s
.
trie
.
cache
.
IsDirty
,
checker
.
Equals
,
false
,
checker
.
Commentf
(
"Expected trie to be dirty"
))
if
trie
.
cache
.
IsDirty
{
t
.
Error
(
"Expected trie not to be dirty"
)
}
s
.
trie
.
Update
(
"test"
,
LONG_WORD
)
s
.
trie
.
cache
.
Undo
()
c
.
Assert
(
s
.
trie
.
cache
.
IsDirty
,
checker
.
Equals
,
false
)
}
}
func
TestTrieReset
(
t
*
testing
.
T
)
{
func
(
s
*
TrieSuite
)
TestTrieReset
(
c
*
checker
.
C
)
{
_
,
trie
:=
NewTrie
()
s
.
trie
.
Update
(
"cat"
,
LONG_WORD
)
c
.
Assert
(
s
.
trie
.
cache
.
nodes
,
checker
.
HasLen
,
1
,
checker
.
Commentf
(
"Expected cached nodes"
))
trie
.
Update
(
"cat"
,
LONG_WORD
)
if
len
(
trie
.
cache
.
nodes
)
==
0
{
t
.
Error
(
"Expected cached nodes"
)
}
trie
.
cache
.
Undo
()
s
.
trie
.
cache
.
Undo
()
c
.
Assert
(
s
.
trie
.
cache
.
nodes
,
checker
.
HasLen
,
0
,
checker
.
Commentf
(
"Expected no nodes after undo"
))
if
len
(
trie
.
cache
.
nodes
)
!=
0
{
t
.
Error
(
"Expected no nodes after undo"
)
}
}
}
func
TestTrieGet
(
t
*
testing
.
T
)
{
func
(
s
*
TrieSuite
)
TestTrieGet
(
c
*
checker
.
C
)
{
_
,
trie
:=
NewTrie
()
s
.
trie
.
Update
(
"cat"
,
LONG_WORD
)
x
:=
s
.
trie
.
Get
(
"cat"
)
trie
.
Update
(
"cat"
,
LONG_WORD
)
c
.
Assert
(
x
,
checker
.
DeepEquals
,
LONG_WORD
)
x
:=
trie
.
Get
(
"cat"
)
if
x
!=
LONG_WORD
{
t
.
Error
(
"expected %s, got %s"
,
LONG_WORD
,
x
)
}
}
}
func
TestTrieUpdating
(
t
*
testing
.
T
)
{
func
(
s
*
TrieSuite
)
TestTrieUpdating
(
c
*
checker
.
C
)
{
_
,
trie
:=
NewTrie
()
s
.
trie
.
Update
(
"cat"
,
LONG_WORD
)
trie
.
Update
(
"cat"
,
LONG_WORD
)
s
.
trie
.
Update
(
"cat"
,
LONG_WORD
+
"1"
)
trie
.
Update
(
"cat"
,
LONG_WORD
+
"1"
)
x
:=
s
.
trie
.
Get
(
"cat"
)
x
:=
trie
.
Get
(
"cat"
)
c
.
Assert
(
x
,
checker
.
DeepEquals
,
LONG_WORD
+
"1"
)
if
x
!=
LONG_WORD
+
"1"
{
t
.
Error
(
"expected %S, got %s"
,
LONG_WORD
+
"1"
,
x
)
}
}
}
func
TestTrieCmp
(
t
*
testing
.
T
)
{
func
(
s
*
TrieSuite
)
TestTrieCmp
(
c
*
checker
.
C
)
{
_
,
trie1
:=
NewTrie
()
_
,
trie1
:=
NewTrie
()
_
,
trie2
:=
NewTrie
()
_
,
trie2
:=
NewTrie
()
trie1
.
Update
(
"doge"
,
LONG_WORD
)
trie1
.
Update
(
"doge"
,
LONG_WORD
)
trie2
.
Update
(
"doge"
,
LONG_WORD
)
trie2
.
Update
(
"doge"
,
LONG_WORD
)
if
!
trie1
.
Cmp
(
trie2
)
{
c
.
Assert
(
trie1
,
checker
.
DeepEquals
,
trie2
)
t
.
Error
(
"Expected tries to be equal"
)
}
trie1
.
Update
(
"dog"
,
LONG_WORD
)
trie1
.
Update
(
"dog"
,
LONG_WORD
)
trie2
.
Update
(
"cat"
,
LONG_WORD
)
trie2
.
Update
(
"cat"
,
LONG_WORD
)
if
trie1
.
Cmp
(
trie2
)
{
c
.
Assert
(
trie1
,
checker
.
Not
(
checker
.
DeepEquals
),
trie2
)
t
.
Errorf
(
"Expected tries not to be equal %x %x"
,
trie1
.
Root
,
trie2
.
Root
)
}
}
}
func
TestTrieDelete
(
t
*
testing
.
T
)
{
func
(
s
*
TrieSuite
)
TestTrieDelete
(
c
*
checker
.
C
)
{
_
,
trie
:=
NewTrie
()
s
.
trie
.
Update
(
"cat"
,
LONG_WORD
)
trie
.
Update
(
"cat"
,
LONG_WORD
)
exp
:=
s
.
trie
.
Root
exp
:=
trie
.
Root
s
.
trie
.
Update
(
"dog"
,
LONG_WORD
)
trie
.
Update
(
"dog"
,
LONG_WORD
)
s
.
trie
.
Delete
(
"dog"
)
trie
.
Delete
(
"dog"
)
c
.
Assert
(
s
.
trie
.
Root
,
checker
.
DeepEquals
,
exp
)
if
!
reflect
.
DeepEqual
(
exp
,
trie
.
Root
)
{
t
.
Errorf
(
"Expected tries to be equal %x : %x"
,
exp
,
trie
.
Root
)
s
.
trie
.
Update
(
"dog"
,
LONG_WORD
)
}
exp
=
s
.
trie
.
Root
s
.
trie
.
Update
(
"dude"
,
LONG_WORD
)
trie
.
Update
(
"dog"
,
LONG_WORD
)
s
.
trie
.
Delete
(
"dude"
)
exp
=
trie
.
Root
c
.
Assert
(
s
.
trie
.
Root
,
checker
.
DeepEquals
,
exp
)
trie
.
Update
(
"dude"
,
LONG_WORD
)
trie
.
Delete
(
"dude"
)
if
!
reflect
.
DeepEqual
(
exp
,
trie
.
Root
)
{
t
.
Errorf
(
"Expected tries to be equal %x : %x"
,
exp
,
trie
.
Root
)
}
}
}
func
TestTrieDeleteWithValue
(
t
*
testing
.
T
)
{
func
(
s
*
TrieSuite
)
TestTrieDeleteWithValue
(
c
*
checker
.
C
)
{
_
,
trie
:=
NewTrie
()
s
.
trie
.
Update
(
"c"
,
LONG_WORD
)
trie
.
Update
(
"c"
,
LONG_WORD
)
exp
:=
s
.
trie
.
Root
exp
:=
trie
.
Root
s
.
trie
.
Update
(
"ca"
,
LONG_WORD
)
trie
.
Update
(
"ca"
,
LONG_WORD
)
s
.
trie
.
Update
(
"cat"
,
LONG_WORD
)
trie
.
Update
(
"cat"
,
LONG_WORD
)
s
.
trie
.
Delete
(
"ca"
)
trie
.
Delete
(
"ca"
)
s
.
trie
.
Delete
(
"cat"
)
trie
.
Delete
(
"cat"
)
c
.
Assert
(
s
.
trie
.
Root
,
checker
.
DeepEquals
,
exp
)
if
!
reflect
.
DeepEqual
(
exp
,
trie
.
Root
)
{
t
.
Errorf
(
"Expected tries to be equal %x : %x"
,
exp
,
trie
.
Root
)
}
}
}
func
TestTriePurge
(
t
*
testing
.
T
)
{
func
(
s
*
TrieSuite
)
TestTriePurge
(
c
*
checker
.
C
)
{
_
,
trie
:=
NewTrie
()
s
.
trie
.
Update
(
"c"
,
LONG_WORD
)
trie
.
Update
(
"c"
,
LONG_WORD
)
s
.
trie
.
Update
(
"ca"
,
LONG_WORD
)
trie
.
Update
(
"ca"
,
LONG_WORD
)
s
.
trie
.
Update
(
"cat"
,
LONG_WORD
)
trie
.
Update
(
"cat"
,
LONG_WORD
)
lenBefore
:=
len
(
trie
.
cache
.
nodes
)
it
:=
trie
.
NewIterator
()
if
num
:=
it
.
Purge
();
num
!=
3
{
t
.
Errorf
(
"Expected purge to return 3, got %d"
,
num
)
}
if
lenBefore
==
len
(
trie
.
cache
.
nodes
)
{
lenBefore
:=
len
(
s
.
trie
.
cache
.
nodes
)
t
.
Errorf
(
"Expected cached nodes to be deleted"
)
it
:=
s
.
trie
.
NewIterator
()
}
num
:=
it
.
Purge
()
c
.
Assert
(
num
,
checker
.
Equals
,
3
)
c
.
Assert
(
len
(
s
.
trie
.
cache
.
nodes
),
checker
.
Equals
,
lenBefore
)
}
}
func
h
(
str
string
)
string
{
func
h
(
str
string
)
string
{
...
@@ -199,23 +159,23 @@ func get(in string) (out string) {
...
@@ -199,23 +159,23 @@ func get(in string) (out string) {
return
return
}
}
type
Test
struct
{
type
T
rieT
est
struct
{
Name
string
Name
string
In
map
[
string
]
string
In
map
[
string
]
string
Root
string
Root
string
}
}
func
CreateTest
(
name
string
,
data
[]
byte
)
(
Test
,
error
)
{
func
CreateTest
(
name
string
,
data
[]
byte
)
(
T
rieT
est
,
error
)
{
t
:=
Test
{
Name
:
name
}
t
:=
T
rieT
est
{
Name
:
name
}
err
:=
json
.
Unmarshal
(
data
,
&
t
)
err
:=
json
.
Unmarshal
(
data
,
&
t
)
if
err
!=
nil
{
if
err
!=
nil
{
return
Test
{},
fmt
.
Errorf
(
"%v"
,
err
)
return
T
rieT
est
{},
fmt
.
Errorf
(
"%v"
,
err
)
}
}
return
t
,
nil
return
t
,
nil
}
}
func
CreateTests
(
uri
string
,
cb
func
(
T
est
))
map
[
string
]
Test
{
func
CreateTests
(
uri
string
,
cb
func
(
T
rieTest
))
map
[
string
]
Trie
Test
{
resp
,
err
:=
http
.
Get
(
uri
)
resp
,
err
:=
http
.
Get
(
uri
)
if
err
!=
nil
{
if
err
!=
nil
{
panic
(
err
)
panic
(
err
)
...
@@ -230,7 +190,7 @@ func CreateTests(uri string, cb func(Test)) map[string]Test {
...
@@ -230,7 +190,7 @@ func CreateTests(uri string, cb func(Test)) map[string]Test {
panic
(
err
)
panic
(
err
)
}
}
tests
:=
make
(
map
[
string
]
Test
)
tests
:=
make
(
map
[
string
]
T
rieT
est
)
for
name
,
testData
:=
range
objmap
{
for
name
,
testData
:=
range
objmap
{
test
,
err
:=
CreateTest
(
name
,
*
testData
)
test
,
err
:=
CreateTest
(
name
,
*
testData
)
if
err
!=
nil
{
if
err
!=
nil
{
...
@@ -274,7 +234,7 @@ func RandomData() [][]string {
...
@@ -274,7 +234,7 @@ func RandomData() [][]string {
const
MaxTest
=
1000
const
MaxTest
=
1000
// This test insert data in random order and seeks to find indifferences between the different tries
// This test insert data in random order and seeks to find indifferences between the different tries
func
TestRegression
(
t
*
testing
.
T
)
{
func
(
s
*
TrieSuite
)
TestRegression
(
c
*
checker
.
C
)
{
rand
.
Seed
(
time
.
Now
()
.
Unix
())
rand
.
Seed
(
time
.
Now
()
.
Unix
())
roots
:=
make
(
map
[
string
]
int
)
roots
:=
make
(
map
[
string
]
int
)
...
@@ -290,34 +250,33 @@ func TestRegression(t *testing.T) {
...
@@ -290,34 +250,33 @@ func TestRegression(t *testing.T) {
roots
[
string
(
trie
.
Root
.
([]
byte
))]
+=
1
roots
[
string
(
trie
.
Root
.
([]
byte
))]
+=
1
}
}
if
len
(
roots
)
>
1
{
c
.
Assert
(
len
(
roots
)
<=
1
,
checker
.
Equals
,
true
)
for
root
,
num
:=
range
roots
{
// if len(roots) > 1 {
t
.
Errorf
(
"%x => %d
\n
"
,
root
,
num
)
// for root, num := range roots {
}
// t.Errorf("%x => %d\n", root, num)
}
// }
// }
}
}
func
TestDelete
(
t
*
testing
.
T
)
{
func
(
s
*
TrieSuite
)
TestDelete
(
c
*
checker
.
C
)
{
_
,
trie
:=
NewTrie
()
s
.
trie
.
Update
(
"a"
,
"jeffreytestlongstring"
)
s
.
trie
.
Update
(
"aa"
,
"otherstring"
)
trie
.
Update
(
"a"
,
"jeffreytestlongstring"
)
s
.
trie
.
Update
(
"aaa"
,
"othermorestring"
)
trie
.
Update
(
"aa"
,
"otherstring"
)
s
.
trie
.
Update
(
"aabbbbccc"
,
"hithere"
)
trie
.
Update
(
"aaa"
,
"othermorestring"
)
s
.
trie
.
Update
(
"abbcccdd"
,
"hstanoehutnaheoustnh"
)
trie
.
Update
(
"aabbbbccc"
,
"hithere"
)
s
.
trie
.
Update
(
"rnthaoeuabbcccdd"
,
"hstanoehutnaheoustnh"
)
trie
.
Update
(
"abbcccdd"
,
"hstanoehutnaheoustnh"
)
s
.
trie
.
Update
(
"rneuabbcccdd"
,
"hstanoehutnaheoustnh"
)
trie
.
Update
(
"rnthaoeuabbcccdd"
,
"hstanoehutnaheoustnh"
)
s
.
trie
.
Update
(
"rneuabboeusntahoeucccdd"
,
"hstanoehutnaheoustnh"
)
trie
.
Update
(
"rneuabbcccdd"
,
"hstanoehutnaheoustnh"
)
s
.
trie
.
Update
(
"rnxabboeusntahoeucccdd"
,
"hstanoehutnaheoustnh"
)
trie
.
Update
(
"rneuabboeusntahoeucccdd"
,
"hstanoehutnaheoustnh"
)
s
.
trie
.
Delete
(
"aaboaestnuhbccc"
)
trie
.
Update
(
"rnxabboeusntahoeucccdd"
,
"hstanoehutnaheoustnh"
)
s
.
trie
.
Delete
(
"a"
)
trie
.
Delete
(
"aaboaestnuhbccc"
)
s
.
trie
.
Update
(
"a"
,
"nthaonethaosentuh"
)
trie
.
Delete
(
"a"
)
s
.
trie
.
Update
(
"c"
,
"shtaosntehua"
)
trie
.
Update
(
"a"
,
"nthaonethaosentuh"
)
s
.
trie
.
Delete
(
"a"
)
trie
.
Update
(
"c"
,
"shtaosntehua"
)
s
.
trie
.
Update
(
"aaaa"
,
"testmegood"
)
trie
.
Delete
(
"a"
)
trie
.
Update
(
"aaaa"
,
"testmegood"
)
_
,
t2
:=
NewTrie
()
_
,
t2
:=
NewTrie
()
trie
.
NewIterator
()
.
Each
(
func
(
key
string
,
v
*
ethutil
.
Value
)
{
s
.
trie
.
NewIterator
()
.
Each
(
func
(
key
string
,
v
*
ethutil
.
Value
)
{
if
key
==
"aaaa"
{
if
key
==
"aaaa"
{
t2
.
Update
(
key
,
v
.
Str
())
t2
.
Update
(
key
,
v
.
Str
())
}
else
{
}
else
{
...
@@ -325,27 +284,22 @@ func TestDelete(t *testing.T) {
...
@@ -325,27 +284,22 @@ func TestDelete(t *testing.T) {
}
}
})
})
a
:=
ethutil
.
NewValue
(
trie
.
Root
)
.
Bytes
()
a
:=
ethutil
.
NewValue
(
s
.
trie
.
Root
)
.
Bytes
()
b
:=
ethutil
.
NewValue
(
t2
.
Root
)
.
Bytes
()
b
:=
ethutil
.
NewValue
(
t2
.
Root
)
.
Bytes
()
if
bytes
.
Compare
(
a
,
b
)
!=
0
{
c
.
Assert
(
a
,
checker
.
DeepEquals
,
b
)
t
.
Errorf
(
"Expected %x and %x to be equal"
,
a
,
b
)
}
}
}
func
TestTerminator
(
t
*
testing
.
T
)
{
func
(
s
*
TrieSuite
)
TestTerminator
(
c
*
checker
.
C
)
{
key
:=
CompactDecode
(
"hello"
)
key
:=
CompactDecode
(
"hello"
)
if
!
HasTerm
(
key
)
{
c
.
Assert
(
HasTerm
(
key
),
checker
.
Equals
,
true
,
checker
.
Commentf
(
"Expected %v to have a terminator"
,
key
))
t
.
Errorf
(
"Expected %v to have a terminator"
,
key
)
}
}
}
func
TestIt
(
t
*
testing
.
T
)
{
func
(
s
*
TrieSuite
)
TestIt
(
c
*
checker
.
C
)
{
_
,
trie
:=
NewTrie
()
s
.
trie
.
Update
(
"cat"
,
"cat"
)
trie
.
Update
(
"cat"
,
"cat"
)
s
.
trie
.
Update
(
"doge"
,
"doge"
)
trie
.
Update
(
"doge"
,
"doge"
)
s
.
trie
.
Update
(
"wallace"
,
"wallace"
)
trie
.
Update
(
"wallace"
,
"wallace"
)
it
:=
s
.
trie
.
Iterator
()
it
:=
trie
.
Iterator
()
inputs
:=
[]
struct
{
inputs
:=
[]
struct
{
In
,
Out
string
In
,
Out
string
...
@@ -361,23 +315,16 @@ func TestIt(t *testing.T) {
...
@@ -361,23 +315,16 @@ func TestIt(t *testing.T) {
for
_
,
test
:=
range
inputs
{
for
_
,
test
:=
range
inputs
{
res
:=
string
(
it
.
Next
(
test
.
In
))
res
:=
string
(
it
.
Next
(
test
.
In
))
if
res
!=
test
.
Out
{
c
.
Assert
(
res
,
checker
.
Equals
,
test
.
Out
)
t
.
Errorf
(
test
.
In
,
"failed. Got"
,
res
,
"Expected"
,
test
.
Out
)
}
}
}
}
}
func
TestBeginsWith
(
t
*
testing
.
T
)
{
func
(
s
*
TrieSuite
)
TestBeginsWith
(
c
*
checker
.
C
)
{
a
:=
CompactDecode
(
"hello"
)
a
:=
CompactDecode
(
"hello"
)
b
:=
CompactDecode
(
"hel"
)
b
:=
CompactDecode
(
"hel"
)
if
BeginsWith
(
a
,
b
)
{
c
.
Assert
(
BeginsWith
(
a
,
b
),
checker
.
Equals
,
false
)
t
.
Errorf
(
"Expected %x to begin with %x"
,
a
,
b
)
c
.
Assert
(
BeginsWith
(
b
,
a
),
checker
.
Equals
,
true
)
}
if
BeginsWith
(
b
,
a
)
{
t
.
Errorf
(
"Expected %x not to begin with %x"
,
b
,
a
)
}
}
}
/*
/*
...
...
This diff is collapsed.
Click to expand it.
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment