mirror of
https://github.com/go-i2p/go-sam-go.git
synced 2025-12-01 09:54:58 -05:00
Update datagram and stream session tests to use inbound and outbound lengths of 1 for improved testing accuracy
This commit is contained in:
@@ -17,7 +17,7 @@ func TestDatagramSession_Dial(t *testing.T) {
|
||||
|
||||
// Create listener session
|
||||
listenerSession, err := NewDatagramSession(sam1, "test_dial_listener", keys1, []string{
|
||||
"inbound.length=0", "outbound.length=0",
|
||||
"inbound.length=1", "outbound.length=1",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create listener session: %v", err)
|
||||
@@ -32,7 +32,7 @@ func TestDatagramSession_Dial(t *testing.T) {
|
||||
|
||||
// Create dialer session
|
||||
dialerSession, err := NewDatagramSession(sam2, "test_dial_dialer", keys2, []string{
|
||||
"inbound.length=0", "outbound.length=0",
|
||||
"inbound.length=1", "outbound.length=1",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create dialer session: %v", err)
|
||||
|
||||
@@ -9,7 +9,7 @@ func TestDatagramSession_Listen(t *testing.T) {
|
||||
defer sam.Close()
|
||||
|
||||
session, err := NewDatagramSession(sam, "test_listen", keys, []string{
|
||||
"inbound.length=0", "outbound.length=0",
|
||||
"inbound.length=1", "outbound.length=1",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create session: %v", err)
|
||||
|
||||
@@ -65,8 +65,8 @@ func TestNewDatagramSession(t *testing.T) {
|
||||
name: "session with small tunnel config",
|
||||
idBase: "test_datagram_small",
|
||||
options: []string{
|
||||
"inbound.length=0",
|
||||
"outbound.length=0",
|
||||
"inbound.length=1",
|
||||
"outbound.length=1",
|
||||
"inbound.lengthVariance=0",
|
||||
"outbound.lengthVariance=0",
|
||||
"inbound.quantity=1",
|
||||
|
||||
@@ -25,8 +25,8 @@ func Test_DatagramServerClient(t *testing.T) {
|
||||
}
|
||||
// fmt.Println("\tServer: My address: " + keys.Addr().Base32())
|
||||
fmt.Println("\tServer: Creating tunnel")
|
||||
// ds, err := sam.NewDatagramSession("DGserverTun", keys, []string{"inbound.length=0", "outbound.length=0", "inbound.lengthVariance=0", "outbound.lengthVariance=0", "inbound.quantity=1", "outbound.quantity=1"}, 0)
|
||||
ds, err := sam.NewDatagramSession("DGserverTun", keys, []string{"inbound.length=0", "outbound.length=0", "inbound.lengthVariance=0", "outbound.lengthVariance=0", "inbound.quantity=1", "outbound.quantity=1"}, 0)
|
||||
// ds, err := sam.NewDatagramSession("DGserverTun", keys, []string{"inbound.length=1", "outbound.length=1", "inbound.lengthVariance=0", "outbound.lengthVariance=0", "inbound.quantity=1", "outbound.quantity=1"}, 0)
|
||||
ds, err := sam.NewDatagramSession("DGserverTun", keys, []string{"inbound.length=1", "outbound.length=1", "inbound.lengthVariance=0", "outbound.lengthVariance=0", "inbound.quantity=1", "outbound.quantity=1"}, 0)
|
||||
if err != nil {
|
||||
fmt.Println("Server: Failed to create tunnel: " + err.Error())
|
||||
t.Fail()
|
||||
@@ -46,8 +46,8 @@ func Test_DatagramServerClient(t *testing.T) {
|
||||
return
|
||||
}
|
||||
fmt.Println("\tClient: Creating tunnel")
|
||||
// ds2, err := sam2.NewDatagramSession("DGclientTun", keys, []string{"inbound.length=0", "outbound.length=0", "inbound.lengthVariance=0", "outbound.lengthVariance=0", "inbound.quantity=1", "outbound.quantity=1"}, 0)
|
||||
ds2, err := sam2.NewDatagramSession("DGclientTun", keys, []string{"inbound.length=0", "outbound.length=0", "inbound.lengthVariance=0", "outbound.lengthVariance=0", "inbound.quantity=1", "outbound.quantity=1"}, 0)
|
||||
// ds2, err := sam2.NewDatagramSession("DGclientTun", keys, []string{"inbound.length=1", "outbound.length=1", "inbound.lengthVariance=0", "outbound.lengthVariance=0", "inbound.quantity=1", "outbound.quantity=1"}, 0)
|
||||
ds2, err := sam2.NewDatagramSession("DGclientTun", keys, []string{"inbound.length=1", "outbound.length=1", "inbound.lengthVariance=0", "outbound.lengthVariance=0", "inbound.quantity=1", "outbound.quantity=1"}, 0)
|
||||
if err != nil {
|
||||
c <- false
|
||||
return
|
||||
|
||||
@@ -9,11 +9,13 @@ import (
|
||||
|
||||
// Example demonstrates basic usage of the sam3 library for I2P connectivity.
|
||||
// This example shows how to establish a SAM connection, generate keys, and create sessions.
|
||||
//
|
||||
// Requirements: This example requires a running I2P router with SAM bridge enabled.
|
||||
func Example() {
|
||||
// Connect to the local I2P SAM bridge
|
||||
sam, err := sam3.NewSAM("127.0.0.1:7656")
|
||||
if err != nil {
|
||||
log.Printf("Cannot connect to I2P: %v", err)
|
||||
fmt.Printf("Cannot connect to I2P: %v", err)
|
||||
return
|
||||
}
|
||||
defer sam.Close()
|
||||
@@ -21,14 +23,14 @@ func Example() {
|
||||
// Generate I2P keys for this session
|
||||
keys, err := sam.NewKeys()
|
||||
if err != nil {
|
||||
log.Printf("Failed to generate keys: %v", err)
|
||||
fmt.Printf("Failed to generate keys: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Create a stream session for TCP-like connections
|
||||
session, err := sam.NewStreamSession("example-session", keys, sam3.Options_Default)
|
||||
if err != nil {
|
||||
log.Printf("Failed to create session: %v", err)
|
||||
fmt.Printf("Failed to create session: %v", err)
|
||||
return
|
||||
}
|
||||
defer session.Close()
|
||||
@@ -38,11 +40,13 @@ func Example() {
|
||||
}
|
||||
|
||||
// ExampleNewSAM demonstrates how to establish a connection to the I2P SAM bridge.
|
||||
//
|
||||
// Requirements: This example requires a running I2P router with SAM bridge enabled.
|
||||
func ExampleNewSAM() {
|
||||
// Connect to the default I2P SAM bridge address
|
||||
sam, err := sam3.NewSAM(sam3.SAMDefaultAddr(""))
|
||||
if err != nil {
|
||||
log.Printf("Cannot connect to I2P: %v", err)
|
||||
fmt.Printf("Cannot connect to I2P: %v", err)
|
||||
return
|
||||
}
|
||||
defer sam.Close()
|
||||
@@ -51,24 +55,26 @@ func ExampleNewSAM() {
|
||||
}
|
||||
|
||||
// ExampleSAM_NewStreamSession demonstrates creating a stream session for reliable connections.
|
||||
//
|
||||
// Requirements: This example requires a running I2P router with SAM bridge enabled.
|
||||
func ExampleSAM_NewStreamSession() {
|
||||
sam, err := sam3.NewSAM("127.0.0.1:7656")
|
||||
if err != nil {
|
||||
log.Printf("Cannot connect to I2P: %v", err)
|
||||
fmt.Printf("Cannot connect to I2P: %v", err)
|
||||
return
|
||||
}
|
||||
defer sam.Close()
|
||||
|
||||
keys, err := sam.NewKeys()
|
||||
if err != nil {
|
||||
log.Printf("Failed to generate keys: %v", err)
|
||||
fmt.Printf("Failed to generate keys: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Create a stream session with default tunnel configuration
|
||||
session, err := sam.NewStreamSession("my-app", keys, sam3.Options_Default)
|
||||
if err != nil {
|
||||
log.Printf("Failed to create stream session: %v", err)
|
||||
fmt.Printf("Failed to create stream session: %v", err)
|
||||
return
|
||||
}
|
||||
defer session.Close()
|
||||
@@ -77,24 +83,26 @@ func ExampleSAM_NewStreamSession() {
|
||||
}
|
||||
|
||||
// ExampleSAM_NewPrimarySession demonstrates creating a primary session for managing sub-sessions.
|
||||
//
|
||||
// Requirements: This example requires a running I2P router with SAM bridge enabled.
|
||||
func ExampleSAM_NewPrimarySession() {
|
||||
sam, err := sam3.NewSAM("127.0.0.1:7656")
|
||||
if err != nil {
|
||||
log.Printf("Cannot connect to I2P: %v", err)
|
||||
fmt.Printf("Cannot connect to I2P: %v", err)
|
||||
return
|
||||
}
|
||||
defer sam.Close()
|
||||
|
||||
keys, err := sam.NewKeys()
|
||||
if err != nil {
|
||||
log.Printf("Failed to generate keys: %v", err)
|
||||
fmt.Printf("Failed to generate keys: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Create a primary session that can manage multiple sub-sessions
|
||||
primary, err := sam.NewPrimarySession("master-session", keys, sam3.Options_Medium)
|
||||
if err != nil {
|
||||
log.Printf("Failed to create primary session: %v", err)
|
||||
fmt.Printf("Failed to create primary session: %v", err)
|
||||
return
|
||||
}
|
||||
defer primary.Close()
|
||||
@@ -104,24 +112,26 @@ func ExampleSAM_NewPrimarySession() {
|
||||
}
|
||||
|
||||
// ExampleSAM_NewDatagramSession demonstrates creating a datagram session for UDP-like messaging.
|
||||
//
|
||||
// Requirements: This example requires a running I2P router with SAM bridge enabled.
|
||||
func ExampleSAM_NewDatagramSession() {
|
||||
sam, err := sam3.NewSAM("127.0.0.1:7656")
|
||||
if err != nil {
|
||||
log.Printf("Cannot connect to I2P: %v", err)
|
||||
fmt.Printf("Cannot connect to I2P: %v", err)
|
||||
return
|
||||
}
|
||||
defer sam.Close()
|
||||
|
||||
keys, err := sam.NewKeys()
|
||||
if err != nil {
|
||||
log.Printf("Failed to generate keys: %v", err)
|
||||
fmt.Printf("Failed to generate keys: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Create a datagram session for authenticated messaging
|
||||
session, err := sam.NewDatagramSession("udp-app", keys, sam3.Options_Small, 0)
|
||||
if err != nil {
|
||||
log.Printf("Failed to create datagram session: %v", err)
|
||||
fmt.Printf("Failed to create datagram session: %v", err)
|
||||
return
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
@@ -67,8 +67,8 @@ func TestNewPrimarySession(t *testing.T) {
|
||||
name: "primary session with small tunnel config",
|
||||
idBase: "test_primary_small",
|
||||
options: []string{
|
||||
"inbound.length=0",
|
||||
"outbound.length=0",
|
||||
"inbound.length=1",
|
||||
"outbound.length=1",
|
||||
"inbound.quantity=1",
|
||||
"outbound.quantity=1",
|
||||
},
|
||||
|
||||
@@ -24,7 +24,7 @@ func Test_PrimaryDatagramServerClient(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
sam, err := earlysam.NewPrimarySession("PrimaryTunnel", keys, []string{"inbound.length=0", "outbound.length=0", "inbound.lengthVariance=0", "outbound.lengthVariance=0", "inbound.quantity=1", "outbound.quantity=1"})
|
||||
sam, err := earlysam.NewPrimarySession("PrimaryTunnel", keys, []string{"inbound.length=1", "outbound.length=1", "inbound.lengthVariance=0", "outbound.lengthVariance=0", "inbound.quantity=1", "outbound.quantity=1"})
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
return
|
||||
@@ -54,8 +54,8 @@ func Test_PrimaryDatagramServerClient(t *testing.T) {
|
||||
return
|
||||
}
|
||||
fmt.Println("\tClient: Creating tunnel")
|
||||
// ds2, err := sam2.NewDatagramSession("PRIMARYClientTunnel", keys, []string{"inbound.length=0", "outbound.length=0", "inbound.lengthVariance=0", "outbound.lengthVariance=0", "inbound.quantity=1", "outbound.quantity=1"}, 0)
|
||||
ds2, err := sam2.NewDatagramSession("PRIMARYClientTunnel", keys, []string{"inbound.length=0", "outbound.length=0", "inbound.lengthVariance=0", "outbound.lengthVariance=0", "inbound.quantity=1", "outbound.quantity=1"}, 0)
|
||||
// ds2, err := sam2.NewDatagramSession("PRIMARYClientTunnel", keys, []string{"inbound.length=1", "outbound.length=1", "inbound.lengthVariance=0", "outbound.lengthVariance=0", "inbound.quantity=1", "outbound.quantity=1"}, 0)
|
||||
ds2, err := sam2.NewDatagramSession("PRIMARYClientTunnel", keys, []string{"inbound.length=1", "outbound.length=1", "inbound.lengthVariance=0", "outbound.lengthVariance=0", "inbound.quantity=1", "outbound.quantity=1"}, 0)
|
||||
if err != nil {
|
||||
c <- false
|
||||
return
|
||||
|
||||
@@ -35,7 +35,7 @@ func Test_PrimaryStreamingDial(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
sam, err := earlysam.NewPrimarySession("PrimaryTunnel", keys, []string{"inbound.length=0", "outbound.length=0", "inbound.lengthVariance=0", "outbound.lengthVariance=0", "inbound.quantity=1", "outbound.quantity=1"})
|
||||
sam, err := earlysam.NewPrimarySession("PrimaryTunnel", keys, []string{"inbound.length=1", "outbound.length=1", "inbound.lengthVariance=0", "outbound.lengthVariance=0", "inbound.quantity=1", "outbound.quantity=1"})
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
return
|
||||
@@ -98,7 +98,7 @@ func Test_PrimaryStreamingServerClient(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
sam, err := earlysam.NewPrimarySession("PrimaryServerClientTunnel", keys, []string{"inbound.length=0", "outbound.length=0", "inbound.lengthVariance=0", "outbound.lengthVariance=0", "inbound.quantity=1", "outbound.quantity=1"})
|
||||
sam, err := earlysam.NewPrimarySession("PrimaryServerClientTunnel", keys, []string{"inbound.length=1", "outbound.length=1", "inbound.lengthVariance=0", "outbound.lengthVariance=0", "inbound.quantity=1", "outbound.quantity=1"})
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
return
|
||||
|
||||
@@ -14,7 +14,7 @@ func TestStreamSession_Listen(t *testing.T) {
|
||||
defer sam.Close()
|
||||
|
||||
session, err := NewStreamSession(sam, "test_listen", keys, []string{
|
||||
"inbound.length=0", "outbound.length=0",
|
||||
"inbound.length=1", "outbound.length=1",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create session: %v", err)
|
||||
|
||||
@@ -49,8 +49,8 @@ func TestNewStreamSession(t *testing.T) {
|
||||
name: "session with small tunnel config",
|
||||
id: "test_stream_small",
|
||||
options: []string{
|
||||
"inbound.length=0",
|
||||
"outbound.length=0",
|
||||
"inbound.length=1",
|
||||
"outbound.length=1",
|
||||
"inbound.lengthVariance=0",
|
||||
"outbound.lengthVariance=0",
|
||||
"inbound.quantity=1",
|
||||
|
||||
@@ -93,7 +93,7 @@ func Test_StreamingServerClient(t *testing.T) {
|
||||
return
|
||||
}
|
||||
fmt.Println("\tServer: Creating tunnel")
|
||||
ss, err := sam.NewStreamSession("serverTun", keys, []string{"inbound.length=0", "outbound.length=0", "inbound.lengthVariance=0", "outbound.lengthVariance=0", "inbound.quantity=1", "outbound.quantity=1"})
|
||||
ss, err := sam.NewStreamSession("serverTun", keys, []string{"inbound.length=1", "outbound.length=1", "inbound.lengthVariance=0", "outbound.lengthVariance=0", "inbound.quantity=1", "outbound.quantity=1"})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -114,7 +114,7 @@ func Test_StreamingServerClient(t *testing.T) {
|
||||
return
|
||||
}
|
||||
fmt.Println("\tClient: Creating tunnel")
|
||||
ss2, err := sam2.NewStreamSession("clientTun", keys, []string{"inbound.length=0", "outbound.length=0", "inbound.lengthVariance=0", "outbound.lengthVariance=0", "inbound.quantity=1", "outbound.quantity=1"})
|
||||
ss2, err := sam2.NewStreamSession("clientTun", keys, []string{"inbound.length=1", "outbound.length=1", "inbound.lengthVariance=0", "outbound.lengthVariance=0", "inbound.quantity=1", "outbound.quantity=1"})
|
||||
if err != nil {
|
||||
c <- false
|
||||
return
|
||||
@@ -160,48 +160,54 @@ func Test_StreamingServerClient(t *testing.T) {
|
||||
func ExampleStreamSession() {
|
||||
// Creates a new StreamingSession, dials to idk.i2p and gets a SAMConn
|
||||
// which behaves just like a normal net.Conn.
|
||||
//
|
||||
// Requirements: This example requires a running I2P router with SAM bridge enabled.
|
||||
|
||||
const samBridge = "127.0.0.1:7656"
|
||||
|
||||
sam, err := NewSAM(samBridge)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
fmt.Printf("Failed to connect to I2P SAM bridge: %v", err)
|
||||
return
|
||||
}
|
||||
defer sam.Close()
|
||||
keys, err := sam.NewKeys()
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
fmt.Printf("Failed to generate I2P keys: %v", err)
|
||||
return
|
||||
}
|
||||
// See the example Option_* variables.
|
||||
ss, err := sam.NewStreamSession("stream_example", keys, Options_Small)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
fmt.Printf("Failed to create stream session: %v", err)
|
||||
return
|
||||
}
|
||||
someone, err := sam.Lookup("idk.i2p")
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
fmt.Printf("Failed to lookup idk.i2p: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
conn, err := ss.DialI2P(someone)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
fmt.Printf("Failed to dial idk.i2p: %v", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
fmt.Println("Sending HTTP GET /")
|
||||
if _, err := conn.Write([]byte("GET /\n")); err != nil {
|
||||
fmt.Println(err.Error())
|
||||
fmt.Printf("Failed to write to connection: %v", err)
|
||||
return
|
||||
}
|
||||
buf := make([]byte, 4096)
|
||||
n, err := conn.Read(buf)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to read from connection: %v", err)
|
||||
return
|
||||
}
|
||||
if !strings.Contains(strings.ToLower(string(buf[:n])), "http") && !strings.Contains(strings.ToLower(string(buf[:n])), "html") {
|
||||
fmt.Printf("Probably failed to StreamSession.DialI2P(idk.i2p)? It replied %d bytes, but nothing that looked like http/html", n)
|
||||
log.Printf("Probably failed to StreamSession.DialI2P(idk.i2p)? It replied %d bytes, but nothing that looked like http/html", n)
|
||||
fmt.Printf("Failed to get HTTP/HTML response from idk.i2p (got %d bytes)", n)
|
||||
return
|
||||
} else {
|
||||
fmt.Println("Read HTTP/HTML from idk.i2p")
|
||||
log.Println("Read HTTP/HTML from idk.i2p")
|
||||
@@ -217,18 +223,20 @@ func ExampleStreamListener() {
|
||||
// One server Accept()ing on a StreamListener, and one client that Dials
|
||||
// through I2P to the server. Server writes "Hello world!" through a SAMConn
|
||||
// (which implements net.Conn) and the client prints the message.
|
||||
//
|
||||
// Requirements: This example requires a running I2P router with SAM bridge enabled.
|
||||
|
||||
const samBridge = "127.0.0.1:7656"
|
||||
|
||||
sam, err := NewSAM(samBridge)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
fmt.Printf("Failed to connect to I2P SAM bridge: %v", err)
|
||||
return
|
||||
}
|
||||
defer sam.Close()
|
||||
keys, err := sam.NewKeys()
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
fmt.Printf("Failed to generate I2P keys: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -238,31 +246,33 @@ func ExampleStreamListener() {
|
||||
go func(server i2pkeys.I2PAddr) {
|
||||
csam, err := NewSAM(samBridge)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
fmt.Printf("Client failed to connect to I2P: %v", err)
|
||||
quit <- false
|
||||
return
|
||||
}
|
||||
defer csam.Close()
|
||||
keys, err := csam.NewKeys()
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
fmt.Printf("Client failed to generate keys: %v", err)
|
||||
quit <- false
|
||||
return
|
||||
}
|
||||
cs, err := csam.NewStreamSession("client_example", keys, Options_Small)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
fmt.Printf("Client failed to create session: %v", err)
|
||||
quit <- false
|
||||
return
|
||||
}
|
||||
conn, err := cs.DialI2P(server)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
fmt.Printf("Client failed to dial server: %v", err)
|
||||
quit <- false
|
||||
return
|
||||
}
|
||||
buf := make([]byte, 256)
|
||||
n, err := conn.Read(buf)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
fmt.Printf("Client failed to read: %v", err)
|
||||
quit <- false
|
||||
return
|
||||
}
|
||||
@@ -272,22 +282,30 @@ func ExampleStreamListener() {
|
||||
|
||||
ss, err := sam.NewStreamSession("server_example", keys, Options_Small)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
fmt.Printf("Failed to create server session: %v", err)
|
||||
return
|
||||
}
|
||||
l, err := ss.Listen()
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
fmt.Printf("Failed to listen: %v", err)
|
||||
return
|
||||
}
|
||||
conn, err := l.Accept()
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
fmt.Printf("Failed to accept connection: %v", err)
|
||||
return
|
||||
}
|
||||
_, err = conn.Write([]byte("Hello world!"))
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to write to client: %v", err)
|
||||
return
|
||||
}
|
||||
conn.Write([]byte("Hello world!"))
|
||||
|
||||
<-quit // waits for client to die, for example only
|
||||
success := <-quit // waits for client to complete
|
||||
if !success {
|
||||
fmt.Printf("Client operation failed")
|
||||
return
|
||||
}
|
||||
|
||||
// Output:
|
||||
// Hello world!
|
||||
|
||||
Reference in New Issue
Block a user