Skip to content
Snippets Groups Projects
Commit 36ee52f3 authored by Jono Wenger's avatar Jono Wenger
Browse files

Fix file transfer tests and comments

parent d6547765
No related branches found
No related tags found
3 merge requests!510Release,!419rewrote the health tracker to both consider if there are waiting rounds and...,!340Project/channels
...@@ -21,8 +21,8 @@ const ( ...@@ -21,8 +21,8 @@ const (
// Name of listener (used for debugging) // Name of listener (used for debugging)
const listenerName = "NewFileTransferListener-Connection" const listenerName = "NewFileTransferListener-Connection"
// listener waits for a message indicating a new file transfer is starting. // listener waits for a message indicating a new file transfer is starting. This
// Adheres to the receive.Listener interface. // structure adheres to the [receive.Listener] interface.
type listener struct { type listener struct {
m *Wrapper m *Wrapper
} }
......
...@@ -19,11 +19,7 @@ const ( ...@@ -19,11 +19,7 @@ const (
type Params struct { type Params struct {
// NotifyUponCompletion indicates if a final notification message is sent // NotifyUponCompletion indicates if a final notification message is sent
// to the recipient on completion of file transfer. If true, the ping is // to the recipient on completion of file transfer. If true, the ping is
NotifyUponCompletion bool // sent.
}
// paramsDisk will be the marshal-able and unmarshalable object.
type paramsDisk struct {
NotifyUponCompletion bool NotifyUponCompletion bool
} }
...@@ -34,8 +30,10 @@ func DefaultParams() Params { ...@@ -34,8 +30,10 @@ func DefaultParams() Params {
} }
} }
// GetParameters returns the default Params, or override with given parameters, // GetParameters returns the default network parameters, or override with given
// if set. // parameters, if set. Returns an error if provided invalid JSON. If the JSON is
// valid but does not match the Params structure, the default parameters will be
// returned.
func GetParameters(params string) (Params, error) { func GetParameters(params string) (Params, error) {
p := DefaultParams() p := DefaultParams()
if len(params) > 0 { if len(params) > 0 {
...@@ -46,23 +44,3 @@ func GetParameters(params string) (Params, error) { ...@@ -46,23 +44,3 @@ func GetParameters(params string) (Params, error) {
} }
return p, nil return p, nil
} }
// MarshalJSON adheres to the json.Marshaler interface.
func (p Params) MarshalJSON() ([]byte, error) {
pDisk := paramsDisk{NotifyUponCompletion: p.NotifyUponCompletion}
return json.Marshal(&pDisk)
}
// UnmarshalJSON adheres to the json.Unmarshaler interface.
func (p *Params) UnmarshalJSON(data []byte) error {
pDisk := paramsDisk{}
err := json.Unmarshal(data, &pDisk)
if err != nil {
return err
}
*p = Params{NotifyUponCompletion: pDisk.NotifyUponCompletion}
return nil
}
////////////////////////////////////////////////////////////////////////////////
// Copyright © 2022 xx foundation //
// //
// Use of this source code is governed by a license that can be found in the //
// LICENSE file. //
////////////////////////////////////////////////////////////////////////////////
package connect
import (
"encoding/json"
"reflect"
"testing"
)
// Tests that DefaultParams returns a Params object with the expected defaults.
func TestDefaultParams(t *testing.T) {
expected := Params{
NotifyUponCompletion: defaultNotifyUponCompletion,
}
received := DefaultParams()
if !reflect.DeepEqual(expected, received) {
t.Errorf("Received Params does not match expected."+
"\nexpected: %+v\nreceived: %+v", expected, received)
}
}
// Tests that GetParameters uses the passed in parameters.
func TestGetParameters(t *testing.T) {
expected := Params{
NotifyUponCompletion: false,
}
expectedData, err := json.Marshal(expected)
if err != nil {
t.Errorf("Failed to JSON marshal expected params: %+v", err)
}
p, err := GetParameters(string(expectedData))
if err != nil {
t.Errorf("Failed get parameters: %+v", err)
}
if !reflect.DeepEqual(expected, p) {
t.Errorf("Received Params does not match expected."+
"\nexpected: %#v\nreceived: %#v", expected, p)
}
}
// Tests that GetParameters returns the default parameters if no params string
// is provided
func TestGetParameters_Default(t *testing.T) {
expected := DefaultParams()
p, err := GetParameters("")
if err != nil {
t.Errorf("Failed get parameters: %+v", err)
}
if !reflect.DeepEqual(expected, p) {
t.Errorf("Received Params does not match expected."+
"\nexpected: %#v\nreceived: %#v", expected, p)
}
}
// Error path: Tests that GetParameters returns an error when the params string
// does not contain a valid JSON representation of Params.
func TestGetParameters_InvalidParamsStringError(t *testing.T) {
_, err := GetParameters("invalid JSON")
if err == nil {
t.Error("Failed get get error for invalid JSON")
}
}
// Tests that a Params object marshalled via json.Marshal and unmarshalled via
// json.Unmarshal matches the original.
func TestParams_JsonMarshalUnmarshal(t *testing.T) {
// Construct a set of params
expected := DefaultParams()
// Marshal the params
data, err := json.Marshal(&expected)
if err != nil {
t.Fatalf("Marshal error: %v", err)
}
// Unmarshal the params object
received := Params{}
err = json.Unmarshal(data, &received)
if err != nil {
t.Fatalf("Unmarshal error: %v", err)
}
if !reflect.DeepEqual(expected, received) {
t.Errorf("Marshalled and unmarshalled Params does not match original."+
"\nexpected: %#v\nreceived: %#v", expected, received)
}
}
...@@ -35,8 +35,8 @@ type Wrapper struct { ...@@ -35,8 +35,8 @@ type Wrapper struct {
conn connection conn connection
} }
// connection interface matches a subset of the connect.Connection methods used // connection interface matches a subset of the [connect.Connection] methods
// by the Wrapper for easier testing. // used by the Wrapper for easier testing.
type connection interface { type connection interface {
GetPartner() partner.Manager GetPartner() partner.Manager
SendE2E(mt catalog.MessageType, payload []byte, params e2e.Params) ( SendE2E(mt catalog.MessageType, payload []byte, params e2e.Params) (
...@@ -118,7 +118,8 @@ func (w *Wrapper) RegisterSentProgressCallback(tid *ftCrypto.TransferID, ...@@ -118,7 +118,8 @@ func (w *Wrapper) RegisterSentProgressCallback(tid *ftCrypto.TransferID,
// addEndMessageToCallback adds the sending of a connection E2E message when // addEndMessageToCallback adds the sending of a connection E2E message when
// the transfer completed to the callback. If NotifyUponCompletion is not set, // the transfer completed to the callback. If NotifyUponCompletion is not set,
// then the message is not sent. // then the message is not sent.
func (w *Wrapper) addEndMessageToCallback(progressCB ft.SentProgressCallback) ft.SentProgressCallback { func (w *Wrapper) addEndMessageToCallback(
progressCB ft.SentProgressCallback) ft.SentProgressCallback {
if !w.p.NotifyUponCompletion { if !w.p.NotifyUponCompletion {
return progressCB return progressCB
} }
......
...@@ -25,7 +25,7 @@ import ( ...@@ -25,7 +25,7 @@ import (
"time" "time"
) )
// Tests that Connection adheres to the connect.Connection interface. // Tests that Connection adheres to the [connect.Connection] interface.
var _ connection = (connect.Connection)(nil) var _ connection = (connect.Connection)(nil)
// Smoke test of the entire file transfer system. // Smoke test of the entire file transfer system.
...@@ -60,7 +60,8 @@ func Test_FileTransfer_Smoke(t *testing.T) { ...@@ -60,7 +60,8 @@ func Test_FileTransfer_Smoke(t *testing.T) {
storage1 := newMockStorage() storage1 := newMockStorage()
endE2eChan1 := make(chan receive.Message, 3) endE2eChan1 := make(chan receive.Message, 3)
conn1 := newMockConnection(myID1, myID2, e2eHandler, t) conn1 := newMockConnection(myID1, myID2, e2eHandler, t)
_, _ = conn1.RegisterListener(catalog.EndFileTransfer, newMockListener(endE2eChan1)) _, _ = conn1.RegisterListener(
catalog.EndFileTransfer, newMockListener(endE2eChan1))
cMix1 := newMockCmix(myID1, cMixHandler, storage1) cMix1 := newMockCmix(myID1, cMixHandler, storage1)
user1 := newMockUser(myID1, cMix1, storage1, rngGen) user1 := newMockUser(myID1, cMix1, storage1, rngGen)
ftManager1, err := ft.NewManager(ftParams, user1) ftManager1, err := ft.NewManager(ftParams, user1)
...@@ -86,7 +87,8 @@ func Test_FileTransfer_Smoke(t *testing.T) { ...@@ -86,7 +87,8 @@ func Test_FileTransfer_Smoke(t *testing.T) {
storage2 := newMockStorage() storage2 := newMockStorage()
endE2eChan2 := make(chan receive.Message, 3) endE2eChan2 := make(chan receive.Message, 3)
conn2 := newMockConnection(myID2, myID1, e2eHandler, t) conn2 := newMockConnection(myID2, myID1, e2eHandler, t)
_, _ = conn2.RegisterListener(catalog.EndFileTransfer, newMockListener(endE2eChan2)) _, _ = conn2.RegisterListener(
catalog.EndFileTransfer, newMockListener(endE2eChan2))
cMix2 := newMockCmix(myID1, cMixHandler, storage2) cMix2 := newMockCmix(myID1, cMixHandler, storage2)
user2 := newMockUser(myID2, cMix2, storage2, rngGen) user2 := newMockUser(myID2, cMix2, storage2, rngGen)
ftManager2, err := ft.NewManager(ftParams, user2) ftManager2, err := ft.NewManager(ftParams, user2)
......
...@@ -21,8 +21,8 @@ const ( ...@@ -21,8 +21,8 @@ const (
// Name of listener (used for debugging) // Name of listener (used for debugging)
const listenerName = "NewFileTransferListener-E2E" const listenerName = "NewFileTransferListener-E2E"
// listener waits for a message indicating a new file transfer is starting. // listener waits for a message indicating a new file transfer is starting. This
// Adheres to the receive.Listener interface. // structure adheres to the [receive.Listener] interface.
type listener struct { type listener struct {
m *Wrapper m *Wrapper
} }
......
...@@ -17,11 +17,7 @@ const ( ...@@ -17,11 +17,7 @@ const (
type Params struct { type Params struct {
// NotifyUponCompletion indicates if a final notification message is sent // NotifyUponCompletion indicates if a final notification message is sent
// to the recipient on completion of file transfer. If true, the ping is // to the recipient on completion of file transfer. If true, the ping is
NotifyUponCompletion bool // sent.
}
// paramsDisk will be the marshal-able and umarshal-able object.
type paramsDisk struct {
NotifyUponCompletion bool NotifyUponCompletion bool
} }
...@@ -32,8 +28,10 @@ func DefaultParams() Params { ...@@ -32,8 +28,10 @@ func DefaultParams() Params {
} }
} }
// GetParameters returns the default Params, or override with given // GetParameters returns the default network parameters, or override with given
// parameters, if set. // parameters, if set. Returns an error if provided invalid JSON. If the JSON is
// valid but does not match the Params structure, the default parameters will be
// returned.
func GetParameters(params string) (Params, error) { func GetParameters(params string) (Params, error) {
p := DefaultParams() p := DefaultParams()
if len(params) > 0 { if len(params) > 0 {
...@@ -44,23 +42,3 @@ func GetParameters(params string) (Params, error) { ...@@ -44,23 +42,3 @@ func GetParameters(params string) (Params, error) {
} }
return p, nil return p, nil
} }
// MarshalJSON adheres to the json.Marshaler interface.
func (p Params) MarshalJSON() ([]byte, error) {
pDisk := paramsDisk{NotifyUponCompletion: p.NotifyUponCompletion}
return json.Marshal(&pDisk)
}
// UnmarshalJSON adheres to the json.Unmarshaler interface.
func (p *Params) UnmarshalJSON(data []byte) error {
pDisk := paramsDisk{}
err := json.Unmarshal(data, &pDisk)
if err != nil {
return err
}
*p = Params{NotifyUponCompletion: pDisk.NotifyUponCompletion}
return nil
}
////////////////////////////////////////////////////////////////////////////////
// Copyright © 2022 xx foundation //
// //
// Use of this source code is governed by a license that can be found in the //
// LICENSE file. //
////////////////////////////////////////////////////////////////////////////////
package e2e
import (
"encoding/json"
"reflect"
"testing"
)
// Tests that DefaultParams returns a Params object with the expected defaults.
func TestDefaultParams(t *testing.T) {
expected := Params{
NotifyUponCompletion: defaultNotifyUponCompletion,
}
received := DefaultParams()
if !reflect.DeepEqual(expected, received) {
t.Errorf("Received Params does not match expected."+
"\nexpected: %+v\nreceived: %+v", expected, received)
}
}
// Tests that GetParameters uses the passed in parameters.
func TestGetParameters(t *testing.T) {
expected := Params{
NotifyUponCompletion: false,
}
expectedData, err := json.Marshal(expected)
if err != nil {
t.Errorf("Failed to JSON marshal expected params: %+v", err)
}
p, err := GetParameters(string(expectedData))
if err != nil {
t.Errorf("Failed get parameters: %+v", err)
}
if !reflect.DeepEqual(expected, p) {
t.Errorf("Received Params does not match expected."+
"\nexpected: %#v\nreceived: %#v", expected, p)
}
}
// Tests that GetParameters returns the default parameters if no params string
// is provided
func TestGetParameters_Default(t *testing.T) {
expected := DefaultParams()
p, err := GetParameters("")
if err != nil {
t.Errorf("Failed get parameters: %+v", err)
}
if !reflect.DeepEqual(expected, p) {
t.Errorf("Received Params does not match expected."+
"\nexpected: %#v\nreceived: %#v", expected, p)
}
}
// Error path: Tests that GetParameters returns an error when the params string
// does not contain a valid JSON representation of Params.
func TestGetParameters_InvalidParamsStringError(t *testing.T) {
_, err := GetParameters("invalid JSON")
if err == nil {
t.Error("Failed get get error for invalid JSON")
}
}
// Tests that a Params object marshalled via json.Marshal and unmarshalled via
// json.Unmarshal matches the original.
func TestParams_JsonMarshalUnmarshal(t *testing.T) {
// Construct a set of params
expected := DefaultParams()
// Marshal the params
data, err := json.Marshal(&expected)
if err != nil {
t.Fatalf("Marshal error: %v", err)
}
// Unmarshal the params object
received := Params{}
err = json.Unmarshal(data, &received)
if err != nil {
t.Fatalf("Unmarshal error: %v", err)
}
if !reflect.DeepEqual(expected, received) {
t.Errorf("Marshalled and unmarshalled Params does not match original."+
"\nexpected: %#v\nreceived: %#v", expected, received)
}
}
...@@ -57,7 +57,8 @@ func sendNewFileTransferMessage( ...@@ -57,7 +57,8 @@ func sendNewFileTransferMessage(
// sendEndFileTransferMessage sends an E2E message to the recipient informing // sendEndFileTransferMessage sends an E2E message to the recipient informing
// them that all file parts have arrived once the network is healthy. // them that all file parts have arrived once the network is healthy.
func sendEndFileTransferMessage(recipient *id.ID, cmix ft.Cmix, e2eHandler e2eHandler) { func sendEndFileTransferMessage(
recipient *id.ID, cmix ft.Cmix, e2eHandler e2eHandler) {
callbackID := make(chan uint64, 1) callbackID := make(chan uint64, 1)
callbackID <- cmix.AddHealthCallback( callbackID <- cmix.AddHealthCallback(
func(healthy bool) { func(healthy bool) {
......
...@@ -35,8 +35,8 @@ type Wrapper struct { ...@@ -35,8 +35,8 @@ type Wrapper struct {
e2e e2eHandler e2e e2eHandler
} }
// e2eHandler interface matches a subset of the e2e.Handler methods used by the Wrapper // e2eHandler interface matches a subset of the [e2e.Handler] methods used by
// for easier testing. // the Wrapper for easier testing.
type e2eHandler interface { type e2eHandler interface {
SendE2E(mt catalog.MessageType, recipient *id.ID, payload []byte, SendE2E(mt catalog.MessageType, recipient *id.ID, payload []byte,
params e2e.Params) (cryptoE2e.SendReport, error) params e2e.Params) (cryptoE2e.SendReport, error)
......
...@@ -50,8 +50,8 @@ type gcManager interface { ...@@ -50,8 +50,8 @@ type gcManager interface {
} }
// NewWrapper generates a new file transfer Wrapper for group chat. // NewWrapper generates a new file transfer Wrapper for group chat.
func NewWrapper(receiveCB ft.ReceiveCallback, ft ft.FileTransfer, gc gcManager) ( func NewWrapper(receiveCB ft.ReceiveCallback, ft ft.FileTransfer,
*Wrapper, error) { gc gcManager) (*Wrapper, error) {
w := &Wrapper{ w := &Wrapper{
receiveCB: receiveCB, receiveCB: receiveCB,
ft: ft, ft: ft,
......
...@@ -204,9 +204,11 @@ func Test_FileTransfer_Smoke(t *testing.T) { ...@@ -204,9 +204,11 @@ func Test_FileTransfer_Smoke(t *testing.T) {
fileName, sendTime, fileSizeKb, throughput) fileName, sendTime, fileSizeKb, throughput)
expectedThroughput := float64(params.MaxThroughput) * .001 expectedThroughput := float64(params.MaxThroughput) * .001
delta := (math.Abs(expectedThroughput-throughput) / ((expectedThroughput + throughput) / 2)) * 100 delta := (math.Abs(expectedThroughput-throughput) /
((expectedThroughput + throughput) / 2)) * 100
t.Logf("Expected bandwidth: %.2f kb/s", expectedThroughput) t.Logf("Expected bandwidth: %.2f kb/s", expectedThroughput)
t.Logf("Bandwidth difference: %.2f kb/s (%.2f%%)", expectedThroughput-throughput, delta) t.Logf("Bandwidth difference: %.2f kb/s (%.2f%%)",
expectedThroughput-throughput, delta)
} }
}() }()
......
...@@ -33,13 +33,6 @@ type Params struct { ...@@ -33,13 +33,6 @@ type Params struct {
Cmix cmix.CMIXParams Cmix cmix.CMIXParams
} }
// paramsDisk will be the marshal-able and umarshal-able object.
type paramsDisk struct {
MaxThroughput int
SendTimeout time.Duration
Cmix cmix.CMIXParams
}
// DefaultParams returns a Params object filled with the default values. // DefaultParams returns a Params object filled with the default values.
func DefaultParams() Params { func DefaultParams() Params {
return Params{ return Params{
...@@ -50,7 +43,9 @@ func DefaultParams() Params { ...@@ -50,7 +43,9 @@ func DefaultParams() Params {
} }
// GetParameters returns the default network parameters, or override with given // GetParameters returns the default network parameters, or override with given
// parameters, if set. // parameters, if set. Returns an error if provided invalid JSON. If the JSON is
// valid but does not match the Params structure, the default parameters will be
// returned.
func GetParameters(params string) (Params, error) { func GetParameters(params string) (Params, error) {
p := DefaultParams() p := DefaultParams()
if len(params) > 0 { if len(params) > 0 {
...@@ -61,32 +56,3 @@ func GetParameters(params string) (Params, error) { ...@@ -61,32 +56,3 @@ func GetParameters(params string) (Params, error) {
} }
return p, nil return p, nil
} }
// MarshalJSON adheres to the json.Marshaler interface.
func (p Params) MarshalJSON() ([]byte, error) {
pDisk := paramsDisk{
MaxThroughput: p.MaxThroughput,
SendTimeout: p.SendTimeout,
Cmix: p.Cmix,
}
return json.Marshal(&pDisk)
}
// UnmarshalJSON adheres to the json.Unmarshaler interface.
func (p *Params) UnmarshalJSON(data []byte) error {
pDisk := paramsDisk{}
err := json.Unmarshal(data, &pDisk)
if err != nil {
return err
}
*p = Params{
MaxThroughput: pDisk.MaxThroughput,
SendTimeout: pDisk.SendTimeout,
Cmix: pDisk.Cmix,
}
return nil
}
...@@ -8,58 +8,111 @@ ...@@ -8,58 +8,111 @@
package fileTransfer package fileTransfer
import ( import (
"bytes"
"encoding/json" "encoding/json"
"gitlab.com/elixxir/client/cmix" "gitlab.com/elixxir/client/cmix"
"reflect" "reflect"
"testing" "testing"
) )
// Tests that no data is lost when marshaling and unmarshalling the Params // Tests that DefaultParams returns a Params object with the expected defaults.
// object. func TestDefaultParams(t *testing.T) {
func TestParams_MarshalUnmarshal(t *testing.T) { expected := Params{
// Construct a set of params MaxThroughput: defaultMaxThroughput,
p := DefaultParams() SendTimeout: defaultSendTimeout,
Cmix: cmix.GetDefaultCMIXParams(),
}
received := DefaultParams()
received.Cmix.Stop = expected.Cmix.Stop
// Marshal the params if !reflect.DeepEqual(expected, received) {
data, err := json.Marshal(&p) t.Errorf("Received Params does not match expected."+
"\nexpected: %+v\nreceived: %+v", expected, received)
}
}
// Tests that GetParameters uses the passed in parameters.
func TestGetParameters(t *testing.T) {
expected := Params{
MaxThroughput: 42,
SendTimeout: 11,
Cmix: cmix.CMIXParams{
RoundTries: 5,
Timeout: 6,
RetryDelay: 7,
ExcludedRounds: nil,
SendTimeout: 8,
DebugTag: "9",
Stop: nil,
BlacklistedNodes: cmix.NodeMap{},
Critical: true,
},
}
expectedData, err := json.Marshal(expected)
if err != nil { if err != nil {
t.Fatalf("Marshal error: %v", err) t.Errorf("Failed to JSON marshal expected params: %+v", err)
} }
// Unmarshal the params object p, err := GetParameters(string(expectedData))
received := Params{}
err = json.Unmarshal(data, &received)
if err != nil { if err != nil {
t.Fatalf("Unmarshal error: %v", err) t.Errorf("Failed get parameters: %+v", err)
}
p.Cmix.Stop = expected.Cmix.Stop
if !reflect.DeepEqual(expected, p) {
t.Errorf("Received Params does not match expected."+
"\nexpected: %#v\nreceived: %#v", expected, p)
}
} }
// Re-marshal this params object // Tests that GetParameters returns the default parameters if no params string
data2, err := json.Marshal(received) // is provided
func TestGetParameters_Default(t *testing.T) {
expected := DefaultParams()
p, err := GetParameters("")
if err != nil { if err != nil {
t.Fatalf("Marshal error: %v", err) t.Errorf("Failed get parameters: %+v", err)
} }
p.Cmix.Stop = expected.Cmix.Stop
// Check that they match (it is done this way to avoid false failures with if !reflect.DeepEqual(expected, p) {
// the reflect.DeepEqual function and pointers) t.Errorf("Received Params does not match expected."+
if !bytes.Equal(data, data2) { "\nexpected: %#v\nreceived: %#v", expected, p)
t.Fatalf("Data was lost in marshal/unmarshal.") }
} }
// Error path: Tests that GetParameters returns an error when the params string
// does not contain a valid JSON representation of Params.
func TestGetParameters_InvalidParamsStringError(t *testing.T) {
_, err := GetParameters("invalid JSON")
if err == nil {
t.Error("Failed get get error for invalid JSON")
}
} }
// Tests that DefaultParams returns a Params object with the expected defaults. // Tests that a Params object marshalled via json.Marshal and unmarshalled via
func TestDefaultParams(t *testing.T) { // json.Unmarshal matches the original.
expected := Params{ func TestParams_JsonMarshalUnmarshal(t *testing.T) {
MaxThroughput: defaultMaxThroughput, // Construct a set of params
SendTimeout: defaultSendTimeout, expected := DefaultParams()
Cmix: cmix.GetDefaultCMIXParams(), expected.Cmix.BlacklistedNodes = cmix.NodeMap{}
// Marshal the params
data, err := json.Marshal(&expected)
if err != nil {
t.Fatalf("Marshal error: %v", err)
}
// Unmarshal the params object
received := Params{}
err = json.Unmarshal(data, &received)
if err != nil {
t.Fatalf("Unmarshal error: %v", err)
} }
received := DefaultParams()
received.Cmix.Stop = expected.Cmix.Stop
received.Cmix.Stop = expected.Cmix.Stop
if !reflect.DeepEqual(expected, received) { if !reflect.DeepEqual(expected, received) {
t.Errorf("Received Params does not match expected."+ t.Errorf("Marshalled and unmarshalled Params does not match original."+
"\nexpected: %+v\nreceived: %+v", expected, received) "\nexpected: %#v\nreceived: %#v", expected, received)
} }
} }
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment