<commit_before>// Copyright Istio 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 main

import (
	"flag"
	"fmt"
	"log"
	"net"
	"net/http"
	"os"
	"os/signal"
	"strings"
	"sync"
	"syscall"

	corev2 "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
	corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
	authv2 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v2"
	authv3 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3"
	typev2 "github.com/envoyproxy/go-control-plane/envoy/type"
	typev3 "github.com/envoyproxy/go-control-plane/envoy/type/v3"
	"github.com/gogo/googleapis/google/rpc"
	"golang.org/x/net/context"
	"google.golang.org/genproto/googleapis/rpc/status"
	"google.golang.org/grpc"
)

const (
	checkHeader   = "x-ext-authz"
	allowedValue  = "allow"
	resultHeader  = "x-ext-authz-check-result"
	resultAllowed = "allowed"
	resultDenied  = "denied"
)

var (
	serviceAccount = flag.String("allow_service_account", "a", "allowed service account, matched against the service account in the source principal from the client certificate")
	httpPort       = flag.String("http", "8000", "HTTP server port")
	grpcPort       = flag.String("grpc", "9000", "gRPC server port")
	denyBody       = fmt.Sprintf("denied by ext_authz for not found header `%s: %s` in the request", checkHeader, allowedValue)
)

type extAuthzServerV2 struct{}
type extAuthzServerV3 struct{}

// ExtAuthzServer implements the ext_authz v2/v3 gRPC and HTTP check request API.
type ExtAuthzServer struct {
	grpcServer *grpc.Server
	httpServer *http.Server
	grpcV2     *extAuthzServerV2
	grpcV3     *extAuthzServerV3
	// For test only
	httpPort chan int
	grpcPort chan int
}

// Check implements gRPC v2 check request.
func (s *extAuthzServerV2) Check(ctx context.Context, request *authv2.CheckRequest) (*authv2.CheckResponse, error) {
	l := fmt.Sprintf("%s%s, attributes: %v\n",
		request.GetAttributes().GetRequest().GetHttp().GetHost(),
		request.GetAttributes().GetRequest().GetHttp().GetPath(),
		request.GetAttributes())
	if allowedValue == request.GetAttributes().GetRequest().GetHttp().GetHeaders()[checkHeader] || strings.HasSuffix(request.GetAttributes().Source.Principal, "/sa/" + *serviceAccount) {
		log.Printf("[gRPCv2][allowed]: %s", l)
		return &authv2.CheckResponse{
			HttpResponse: &authv2.CheckResponse_OkResponse{
				OkResponse: &authv2.OkHttpResponse{
					Headers: []*corev2.HeaderValueOption{
						{
							Header: &corev2.HeaderValue{
								Key:   resultHeader,
								Value: resultAllowed,
							},
						},
					},
				},
			},
			Status: &status.Status{Code: int32(rpc.OK)},
		}, nil
	}

	log.Printf("[gRPCv2][denied]: %s", l)
	return &authv2.CheckResponse{
		HttpResponse: &authv2.CheckResponse_DeniedResponse{
			DeniedResponse: &authv2.DeniedHttpResponse{
				Status: &typev2.HttpStatus{Code: typev2.StatusCode_Forbidden},
				Body:   denyBody,
				Headers: []*corev2.HeaderValueOption{
					{
						Header: &corev2.HeaderValue{
							Key:   resultHeader,
							Value: resultDenied,
						},
					},
				},
			},
		},
		Status: &status.Status{Code: int32(rpc.PERMISSION_DENIED)},
	}, nil
}

// Check implements gRPC v3 check request.
func (s *extAuthzServerV3) Check(ctx context.Context, request *authv3.CheckRequest) (*authv3.CheckResponse, error) {
	l := fmt.Sprintf("%s%s, attributes: %v\n",
		request.GetAttributes().GetRequest().GetHttp().GetHost(),
		request.GetAttributes().GetRequest().GetHttp().GetPath(),
		request.GetAttributes())
	if allowedValue == request.GetAttributes().GetRequest().GetHttp().GetHeaders()[checkHeader]  || strings.HasSuffix(request.GetAttributes().Source.Principal, "/sa/" + *serviceAccount) {
		log.Printf("[gRPCv3][allowed]: %s", l)
		return &authv3.CheckResponse{
			HttpResponse: &authv3.CheckResponse_OkResponse{
				OkResponse: &authv3.OkHttpResponse{
					Headers: []*corev3.HeaderValueOption{
						{
							Header: &corev3.HeaderValue{
								Key:   resultHeader,
								Value: resultAllowed,
							},
						},
					},
				},
			},
			Status: &status.Status{Code: int32(rpc.OK)},
		}, nil
	}

	log.Printf("[gRPCv3][denied]: %s", l)
	return &authv3.CheckResponse{
		HttpResponse: &authv3.CheckResponse_DeniedResponse{
			DeniedResponse: &authv3.DeniedHttpResponse{
				Status: &typev3.HttpStatus{Code: typev3.StatusCode_Forbidden},
				Body:   denyBody,
				Headers: []*corev3.HeaderValueOption{
					{
						Header: &corev3.HeaderValue{
							Key:   resultHeader,
							Value: resultDenied,
						},
					},
				},
			},
		},
		Status: &status.Status{Code: int32(rpc.PERMISSION_DENIED)},
	}, nil
}

// ServeHTTP implements the HTTP check request.
func (s *ExtAuthzServer) ServeHTTP(response http.ResponseWriter, request *http.Request) {
	l := fmt.Sprintf("%s %s%s, headers: %v\n", request.Method, request.Host, request.URL, request.Header)
	if allowedValue == request.Header.Get(checkHeader) {
		log.Printf("[HTTP][allowed]: %s", l)
		response.Header().Set(resultHeader, resultAllowed)
		response.WriteHeader(http.StatusOK)
	} else {
		log.Printf("[HTTP][denied]: %s", l)
		response.Header().Set(resultHeader, resultDenied)
		response.WriteHeader(http.StatusForbidden)
		response.Write([]byte(denyBody))
	}
}

func (s *ExtAuthzServer) startGRPC(address string, wg *sync.WaitGroup) {
	defer func() {
		wg.Done()
		log.Printf("Stopped gRPC server")
	}()

	listener, err := net.Listen("tcp", address)
	if err != nil {
		log.Fatalf("Failed to start gRPC server: %v", err)
		return
	}
	// Store the port for test only.
	s.grpcPort <- listener.Addr().(*net.TCPAddr).Port

	s.grpcServer = grpc.NewServer()
	authv2.RegisterAuthorizationServer(s.grpcServer, s.grpcV2)
	authv3.RegisterAuthorizationServer(s.grpcServer, s.grpcV3)

	log.Printf("Starting gRPC server at %s", listener.Addr())
	if err := s.grpcServer.Serve(listener); err != nil {
		log.Fatalf("Failed to serve gRPC server: %v", err)
		return
	}
}

func (s *ExtAuthzServer) startHTTP(address string, wg *sync.WaitGroup) {
	defer func() {
		wg.Done()
		log.Printf("Stopped HTTP server")
	}()

	listener, err := net.Listen("tcp", address)
	if err != nil {
		log.Fatalf("Failed to create HTTP server: %v", err)
	}
	// Store the port for test only.
	s.httpPort <- listener.Addr().(*net.TCPAddr).Port
	s.httpServer = &http.Server{Handler: s}

	log.Printf("Starting HTTP server at %s", listener.Addr())
	if err := s.httpServer.Serve(listener); err != nil {
		log.Fatalf("Failed to start HTTP server: %v", err)
	}
}

func (s *ExtAuthzServer) run(httpAddr, grpcAddr string) {
	var wg sync.WaitGroup
	wg.Add(2)
	go s.startHTTP(httpAddr, &wg)
	go s.startGRPC(grpcAddr, &wg)
	wg.Wait()
}

func (s *ExtAuthzServer) stop() {
	s.grpcServer.Stop()
	log.Printf("GRPC server stopped")
	log.Printf("HTTP server stopped: %v", s.httpServer.Close())
}

func NewExtAuthzServer() *ExtAuthzServer {
	return &ExtAuthzServer{
		grpcV2:   &extAuthzServerV2{},
		grpcV3:   &extAuthzServerV3{},
		httpPort: make(chan int, 1),
		grpcPort: make(chan int, 1),
	}
}

func main() {
	flag.Parse()
	s := NewExtAuthzServer()
	go s.run(fmt.Sprintf(":%s", *httpPort), fmt.Sprintf(":%s", *grpcPort))
	defer s.stop()

	// Wait for the process to be shutdown.
	sigs := make(chan os.Signal, 1)
	signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
	<-sigs
}
<commit_msg>fix nil error (#31504)<commit_after>// Copyright Istio 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 main

import (
	"flag"
	"fmt"
	"log"
	"net"
	"net/http"
	"os"
	"os/signal"
	"strings"
	"sync"
	"syscall"

	corev2 "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
	corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
	authv2 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v2"
	authv3 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3"
	typev2 "github.com/envoyproxy/go-control-plane/envoy/type"
	typev3 "github.com/envoyproxy/go-control-plane/envoy/type/v3"
	"github.com/gogo/googleapis/google/rpc"
	"golang.org/x/net/context"
	"google.golang.org/genproto/googleapis/rpc/status"
	"google.golang.org/grpc"
)

const (
	checkHeader   = "x-ext-authz"
	allowedValue  = "allow"
	resultHeader  = "x-ext-authz-check-result"
	resultAllowed = "allowed"
	resultDenied  = "denied"
)

var (
	serviceAccount = flag.String("allow_service_account", "a", "allowed service account, matched against the service account in the source principal from the client certificate")
	httpPort       = flag.String("http", "8000", "HTTP server port")
	grpcPort       = flag.String("grpc", "9000", "gRPC server port")
	denyBody       = fmt.Sprintf("denied by ext_authz for not found header `%s: %s` in the request", checkHeader, allowedValue)
)

type extAuthzServerV2 struct{}
type extAuthzServerV3 struct{}

// ExtAuthzServer implements the ext_authz v2/v3 gRPC and HTTP check request API.
type ExtAuthzServer struct {
	grpcServer *grpc.Server
	httpServer *http.Server
	grpcV2     *extAuthzServerV2
	grpcV3     *extAuthzServerV3
	// For test only
	httpPort chan int
	grpcPort chan int
}

// Check implements gRPC v2 check request.
func (s *extAuthzServerV2) Check(ctx context.Context, request *authv2.CheckRequest) (*authv2.CheckResponse, error) {
	l := fmt.Sprintf("%s%s, attributes: %v\n",
		request.GetAttributes().GetRequest().GetHttp().GetHost(),
		request.GetAttributes().GetRequest().GetHttp().GetPath(),
		request.GetAttributes())
	if allowedValue == request.GetAttributes().GetRequest().GetHttp().GetHeaders()[checkHeader] || (request.GetAttributes().Source != nil && strings.HasSuffix(request.GetAttributes().Source.Principal, "/sa/"+*serviceAccount)) {
		log.Printf("[gRPCv2][allowed]: %s", l)
		return &authv2.CheckResponse{
			HttpResponse: &authv2.CheckResponse_OkResponse{
				OkResponse: &authv2.OkHttpResponse{
					Headers: []*corev2.HeaderValueOption{
						{
							Header: &corev2.HeaderValue{
								Key:   resultHeader,
								Value: resultAllowed,
							},
						},
					},
				},
			},
			Status: &status.Status{Code: int32(rpc.OK)},
		}, nil
	}

	log.Printf("[gRPCv2][denied]: %s", l)
	return &authv2.CheckResponse{
		HttpResponse: &authv2.CheckResponse_DeniedResponse{
			DeniedResponse: &authv2.DeniedHttpResponse{
				Status: &typev2.HttpStatus{Code: typev2.StatusCode_Forbidden},
				Body:   denyBody,
				Headers: []*corev2.HeaderValueOption{
					{
						Header: &corev2.HeaderValue{
							Key:   resultHeader,
							Value: resultDenied,
						},
					},
				},
			},
		},
		Status: &status.Status{Code: int32(rpc.PERMISSION_DENIED)},
	}, nil
}

// Check implements gRPC v3 check request.
func (s *extAuthzServerV3) Check(ctx context.Context, request *authv3.CheckRequest) (*authv3.CheckResponse, error) {
	l := fmt.Sprintf("%s%s, attributes: %v\n",
		request.GetAttributes().GetRequest().GetHttp().GetHost(),
		request.GetAttributes().GetRequest().GetHttp().GetPath(),
		request.GetAttributes())
	if allowedValue == request.GetAttributes().GetRequest().GetHttp().GetHeaders()[checkHeader] || (request.GetAttributes().Source != nil && strings.HasSuffix(request.GetAttributes().Source.Principal, "/sa/"+*serviceAccount)) {
		log.Printf("[gRPCv3][allowed]: %s", l)
		return &authv3.CheckResponse{
			HttpResponse: &authv3.CheckResponse_OkResponse{
				OkResponse: &authv3.OkHttpResponse{
					Headers: []*corev3.HeaderValueOption{
						{
							Header: &corev3.HeaderValue{
								Key:   resultHeader,
								Value: resultAllowed,
							},
						},
					},
				},
			},
			Status: &status.Status{Code: int32(rpc.OK)},
		}, nil
	}

	log.Printf("[gRPCv3][denied]: %s", l)
	return &authv3.CheckResponse{
		HttpResponse: &authv3.CheckResponse_DeniedResponse{
			DeniedResponse: &authv3.DeniedHttpResponse{
				Status: &typev3.HttpStatus{Code: typev3.StatusCode_Forbidden},
				Body:   denyBody,
				Headers: []*corev3.HeaderValueOption{
					{
						Header: &corev3.HeaderValue{
							Key:   resultHeader,
							Value: resultDenied,
						},
					},
				},
			},
		},
		Status: &status.Status{Code: int32(rpc.PERMISSION_DENIED)},
	}, nil
}

// ServeHTTP implements the HTTP check request.
func (s *ExtAuthzServer) ServeHTTP(response http.ResponseWriter, request *http.Request) {
	l := fmt.Sprintf("%s %s%s, headers: %v\n", request.Method, request.Host, request.URL, request.Header)
	if allowedValue == request.Header.Get(checkHeader) {
		log.Printf("[HTTP][allowed]: %s", l)
		response.Header().Set(resultHeader, resultAllowed)
		response.WriteHeader(http.StatusOK)
	} else {
		log.Printf("[HTTP][denied]: %s", l)
		response.Header().Set(resultHeader, resultDenied)
		response.WriteHeader(http.StatusForbidden)
		response.Write([]byte(denyBody))
	}
}

func (s *ExtAuthzServer) startGRPC(address string, wg *sync.WaitGroup) {
	defer func() {
		wg.Done()
		log.Printf("Stopped gRPC server")
	}()

	listener, err := net.Listen("tcp", address)
	if err != nil {
		log.Fatalf("Failed to start gRPC server: %v", err)
		return
	}
	// Store the port for test only.
	s.grpcPort <- listener.Addr().(*net.TCPAddr).Port

	s.grpcServer = grpc.NewServer()
	authv2.RegisterAuthorizationServer(s.grpcServer, s.grpcV2)
	authv3.RegisterAuthorizationServer(s.grpcServer, s.grpcV3)

	log.Printf("Starting gRPC server at %s", listener.Addr())
	if err := s.grpcServer.Serve(listener); err != nil {
		log.Fatalf("Failed to serve gRPC server: %v", err)
		return
	}
}

func (s *ExtAuthzServer) startHTTP(address string, wg *sync.WaitGroup) {
	defer func() {
		wg.Done()
		log.Printf("Stopped HTTP server")
	}()

	listener, err := net.Listen("tcp", address)
	if err != nil {
		log.Fatalf("Failed to create HTTP server: %v", err)
	}
	// Store the port for test only.
	s.httpPort <- listener.Addr().(*net.TCPAddr).Port
	s.httpServer = &http.Server{Handler: s}

	log.Printf("Starting HTTP server at %s", listener.Addr())
	if err := s.httpServer.Serve(listener); err != nil {
		log.Fatalf("Failed to start HTTP server: %v", err)
	}
}

func (s *ExtAuthzServer) run(httpAddr, grpcAddr string) {
	var wg sync.WaitGroup
	wg.Add(2)
	go s.startHTTP(httpAddr, &wg)
	go s.startGRPC(grpcAddr, &wg)
	wg.Wait()
}

func (s *ExtAuthzServer) stop() {
	s.grpcServer.Stop()
	log.Printf("GRPC server stopped")
	log.Printf("HTTP server stopped: %v", s.httpServer.Close())
}

func NewExtAuthzServer() *ExtAuthzServer {
	return &ExtAuthzServer{
		grpcV2:   &extAuthzServerV2{},
		grpcV3:   &extAuthzServerV3{},
		httpPort: make(chan int, 1),
		grpcPort: make(chan int, 1),
	}
}

func main() {
	flag.Parse()
	s := NewExtAuthzServer()
	go s.run(fmt.Sprintf(":%s", *httpPort), fmt.Sprintf(":%s", *grpcPort))
	defer s.stop()

	// Wait for the process to be shutdown.
	sigs := make(chan os.Signal, 1)
	signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
	<-sigs
}
<|endoftext|>


<commit_before>// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package user

import (
	"runtime"
	"testing"
)

func checkUser(t *testing.T) {
	t.Helper()
	if !userImplemented {
		t.Skip("user: not implemented; skipping tests")
	}
}

func TestCurrent(t *testing.T) {
	u, err := Current()
	if err != nil {
		t.Fatalf("Current: %v (got %#v)", err, u)
	}
	if u.HomeDir == "" {
		t.Errorf("didn't get a HomeDir")
	}
	if u.Username == "" {
		t.Errorf("didn't get a username")
	}
}

func BenchmarkCurrent(b *testing.B) {
	for i := 0; i < b.N; i++ {
		Current()
	}
}

func compare(t *testing.T, want, got *User) {
	if want.Uid != got.Uid {
		t.Errorf("got Uid=%q; want %q", got.Uid, want.Uid)
	}
	if want.Username != got.Username {
		t.Errorf("got Username=%q; want %q", got.Username, want.Username)
	}
	if want.Name != got.Name {
		t.Errorf("got Name=%q; want %q", got.Name, want.Name)
	}
	if want.HomeDir != got.HomeDir {
		t.Errorf("got HomeDir=%q; want %q", got.HomeDir, want.HomeDir)
	}
	if want.Gid != got.Gid {
		t.Errorf("got Gid=%q; want %q", got.Gid, want.Gid)
	}
}

func TestLookup(t *testing.T) {
	checkUser(t)

	if runtime.GOOS == "plan9" {
		t.Skipf("Lookup not implemented on %q", runtime.GOOS)
	}

	want, err := Current()
	if err != nil {
		t.Fatalf("Current: %v", err)
	}
	// TODO: Lookup() has a fast path that calls Current() and returns if the
	// usernames match, so this test does not exercise very much. It would be
	// good to try and test finding a different user than the current user.
	got, err := Lookup(want.Username)
	if err != nil {
		t.Fatalf("Lookup: %v", err)
	}
	compare(t, want, got)
}

func TestLookupId(t *testing.T) {
	checkUser(t)

	if runtime.GOOS == "plan9" {
		t.Skipf("LookupId not implemented on %q", runtime.GOOS)
	}

	want, err := Current()
	if err != nil {
		t.Fatalf("Current: %v", err)
	}
	got, err := LookupId(want.Uid)
	if err != nil {
		t.Fatalf("LookupId: %v", err)
	}
	compare(t, want, got)
}

func checkGroup(t *testing.T) {
	t.Helper()
	if !groupImplemented {
		t.Skip("user: group not implemented; skipping test")
	}
}

func TestLookupGroup(t *testing.T) {
	checkGroup(t)
	user, err := Current()
	if err != nil {
		t.Fatalf("Current(): %v", err)
	}

	g1, err := LookupGroupId(user.Gid)
	if err != nil {
		// NOTE(rsc): Maybe the group isn't defined. That's fine.
		// On my OS X laptop, rsc logs in with group 5000 even
		// though there's no name for group 5000. Such is Unix.
		t.Logf("LookupGroupId(%q): %v", user.Gid, err)
		return
	}
	if g1.Gid != user.Gid {
		t.Errorf("LookupGroupId(%q).Gid = %s; want %s", user.Gid, g1.Gid, user.Gid)
	}

	g2, err := LookupGroup(g1.Name)
	if err != nil {
		t.Fatalf("LookupGroup(%q): %v", g1.Name, err)
	}
	if g1.Gid != g2.Gid || g1.Name != g2.Name {
		t.Errorf("LookupGroup(%q) = %+v; want %+v", g1.Name, g2, g1)
	}
}

func TestGroupIds(t *testing.T) {
	checkGroup(t)
	if runtime.GOOS == "solaris" {
		t.Skip("skipping GroupIds, see golang.org/issue/14709")
	}
	user, err := Current()
	if err != nil {
		t.Fatalf("Current(): %v", err)
	}
	gids, err := user.GroupIds()
	if err != nil {
		t.Fatalf("%+v.GroupIds(): %v", user, err)
	}
	if !containsID(gids, user.Gid) {
		t.Errorf("%+v.GroupIds() = %v; does not contain user GID %s", user, gids, user.Gid)
	}
}

func containsID(ids []string, id string) bool {
	for _, x := range ids {
		if x == id {
			return true
		}
	}
	return false
}
<commit_msg>os/user: disable TestGroupIds for AIX<commit_after>// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package user

import (
	"runtime"
	"testing"
)

func checkUser(t *testing.T) {
	t.Helper()
	if !userImplemented {
		t.Skip("user: not implemented; skipping tests")
	}
}

func TestCurrent(t *testing.T) {
	u, err := Current()
	if err != nil {
		t.Fatalf("Current: %v (got %#v)", err, u)
	}
	if u.HomeDir == "" {
		t.Errorf("didn't get a HomeDir")
	}
	if u.Username == "" {
		t.Errorf("didn't get a username")
	}
}

func BenchmarkCurrent(b *testing.B) {
	for i := 0; i < b.N; i++ {
		Current()
	}
}

func compare(t *testing.T, want, got *User) {
	if want.Uid != got.Uid {
		t.Errorf("got Uid=%q; want %q", got.Uid, want.Uid)
	}
	if want.Username != got.Username {
		t.Errorf("got Username=%q; want %q", got.Username, want.Username)
	}
	if want.Name != got.Name {
		t.Errorf("got Name=%q; want %q", got.Name, want.Name)
	}
	if want.HomeDir != got.HomeDir {
		t.Errorf("got HomeDir=%q; want %q", got.HomeDir, want.HomeDir)
	}
	if want.Gid != got.Gid {
		t.Errorf("got Gid=%q; want %q", got.Gid, want.Gid)
	}
}

func TestLookup(t *testing.T) {
	checkUser(t)

	if runtime.GOOS == "plan9" {
		t.Skipf("Lookup not implemented on %q", runtime.GOOS)
	}

	want, err := Current()
	if err != nil {
		t.Fatalf("Current: %v", err)
	}
	// TODO: Lookup() has a fast path that calls Current() and returns if the
	// usernames match, so this test does not exercise very much. It would be
	// good to try and test finding a different user than the current user.
	got, err := Lookup(want.Username)
	if err != nil {
		t.Fatalf("Lookup: %v", err)
	}
	compare(t, want, got)
}

func TestLookupId(t *testing.T) {
	checkUser(t)

	if runtime.GOOS == "plan9" {
		t.Skipf("LookupId not implemented on %q", runtime.GOOS)
	}

	want, err := Current()
	if err != nil {
		t.Fatalf("Current: %v", err)
	}
	got, err := LookupId(want.Uid)
	if err != nil {
		t.Fatalf("LookupId: %v", err)
	}
	compare(t, want, got)
}

func checkGroup(t *testing.T) {
	t.Helper()
	if !groupImplemented {
		t.Skip("user: group not implemented; skipping test")
	}
}

func TestLookupGroup(t *testing.T) {
	checkGroup(t)
	user, err := Current()
	if err != nil {
		t.Fatalf("Current(): %v", err)
	}

	g1, err := LookupGroupId(user.Gid)
	if err != nil {
		// NOTE(rsc): Maybe the group isn't defined. That's fine.
		// On my OS X laptop, rsc logs in with group 5000 even
		// though there's no name for group 5000. Such is Unix.
		t.Logf("LookupGroupId(%q): %v", user.Gid, err)
		return
	}
	if g1.Gid != user.Gid {
		t.Errorf("LookupGroupId(%q).Gid = %s; want %s", user.Gid, g1.Gid, user.Gid)
	}

	g2, err := LookupGroup(g1.Name)
	if err != nil {
		t.Fatalf("LookupGroup(%q): %v", g1.Name, err)
	}
	if g1.Gid != g2.Gid || g1.Name != g2.Name {
		t.Errorf("LookupGroup(%q) = %+v; want %+v", g1.Name, g2, g1)
	}
}

func TestGroupIds(t *testing.T) {
	checkGroup(t)
	if runtime.GOOS == "aix" {
		t.Skip("skipping GroupIds, see golang.org/issue/30563")
	}
	if runtime.GOOS == "solaris" {
		t.Skip("skipping GroupIds, see golang.org/issue/14709")
	}
	user, err := Current()
	if err != nil {
		t.Fatalf("Current(): %v", err)
	}
	gids, err := user.GroupIds()
	if err != nil {
		t.Fatalf("%+v.GroupIds(): %v", user, err)
	}
	if !containsID(gids, user.Gid) {
		t.Errorf("%+v.GroupIds() = %v; does not contain user GID %s", user, gids, user.Gid)
	}
}

func containsID(ids []string, id string) bool {
	for _, x := range ids {
		if x == id {
			return true
		}
	}
	return false
}
<|endoftext|>


<commit_before>// Copyright 2014 by tkr@ecix.net (Peering GmbH)
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.

package clustersql

import (
	"database/sql"
	"fmt"
	"github.com/BurntSushi/toml"
	"github.com/go-sql-driver/mysql"
	"os"
	"sync"
	"sync/atomic"
	"testing"
)

var db *sql.DB

type NodeCfg struct {
	Name     string
	HostName string
	Port     int
	UserName string
	Password string
	DBName   string
}

type Config struct {
	Nodes []NodeCfg
}

func TestOpen(t *testing.T) {
	cfgfile := os.Getenv("DBCONFIG")
	if cfgfile == "" {
		cfgfile = "config.toml"
	}
	cfg := new(Config)
	if f, err := os.Open(cfgfile); err != nil {
		t.Fatal(err, "(did you set the DBCONFIG env variable?)")
	} else {
		if _, err := toml.DecodeReader(f, cfg); err != nil {
			t.Fatal(err)
		}
	}

	d := NewDriver(mysql.MySQLDriver{})

	for _, ncfg := range cfg.Nodes {
		if ncfg.Password != "" {
			d.AddNode(ncfg.Name, fmt.Sprintf("%s:%s@tcp(%s:%d)/%s", ncfg.UserName, ncfg.Password, ncfg.HostName, ncfg.Port, ncfg.DBName))
		} else {
			d.AddNode(ncfg.Name, fmt.Sprintf("%s@tcp(%s:%d)/%s", ncfg.UserName, ncfg.HostName, ncfg.Port, ncfg.DBName))
		}
	}

	sql.Register("cluster", d)
	var err error
	db, err = sql.Open("cluster", "galera")
	if err != nil {
		t.Error(err)
	}
}

func TestDrop(t *testing.T) {
	_, err := db.Exec("DROP TABLE IF EXISTS test")
	if err != nil {
		t.Fatalf(err.Error())
	}
}

func TestCreate(t *testing.T) {
	_, err := db.Exec("CREATE TABLE test (value BOOL)")
	if err != nil {
		t.Fatalf(err.Error())
	}
}

func TestEmptySelect(t *testing.T) {
	rows, err := db.Query("SELECT * FROM test")
	if err != nil {
		t.Fatalf(err.Error())
	}
	if rows.Next() {
		t.Fatalf("unexpected data in empty table")
	}
}

func TestInsert(t *testing.T) {
	res, err := db.Exec("INSERT INTO test VALUES (1)")
	if err != nil {
		t.Fatalf(err.Error())
	}
	count, err := res.RowsAffected()
	if err != nil {
		t.Fatalf("res.RowsAffected() returned error: %s", err.Error())
	}
	if count != 1 {
		t.Fatalf("Expected 1 affected row, got %d", count)
	}
	id, err := res.LastInsertId()
	if err != nil {
		t.Fatalf("res.LastInsertId() returned error: %s", err.Error())
	}
	if id != 0 {
		t.Fatalf("Expected InsertID 0, got %d", id)
	}
}

func TestSelect(t *testing.T) {
	rows, err := db.Query("SELECT value FROM test")
	if err != nil {
		t.Fatalf(err.Error())
	}
	if rows.Next() {
		var out bool
		rows.Scan(&out)
		if true != out {
			t.Errorf("true != %t", out)
		}
		if rows.Next() {
			t.Error("unexpected data")
		}
	} else {
		t.Error("no data")
	}
}

func TestUpdate(t *testing.T) {
	res, err := db.Exec("UPDATE test SET value = ? WHERE value = ?", false, true)
	if err != nil {
		t.Fatalf(err.Error())
	}
	count, err := res.RowsAffected()
	if err != nil {
		t.Fatalf("res.RowsAffected() returned error: %s", err.Error())
	}
	if count != 1 {
		t.Fatalf("Expected 1 affected row, got %d", count)
	}

	rows, err := db.Query("SELECT value FROM test")
	if err != nil {
		t.Fatalf(err.Error())
	}
	if rows.Next() {
		var out bool
		rows.Scan(&out)
		if false != out {
			t.Errorf("false != %t", out)
		}

		if rows.Next() {
			t.Error("unexpected data")
		}
	} else {
		t.Error("no data")
	}
}

func TestDelete(t *testing.T) {
	res, err := db.Exec("DELETE FROM test WHERE value = ?", false)
	if err != nil {
		t.Fatalf(err.Error())
	}
	count, err := res.RowsAffected()
	if err != nil {
		t.Fatalf("res.RowsAffected() returned error: %s", err.Error())
	}
	if count != 1 {
		t.Fatalf("Expected 1 affected row, got %d", count)
	}

	// Check for unexpected rows
	res, err = db.Exec("DELETE FROM test")
	if err != nil {
		t.Fatalf(err.Error())
	}
	count, err = res.RowsAffected()
	if err != nil {
		t.Fatalf("res.RowsAffected() returned error: %s", err.Error())
	}
	if count != 0 {
		t.Fatalf("Expected 0 affected row, got %d", count)
	}
}

func TestConcurrent(t *testing.T) {
	var max int
	err := db.QueryRow("SELECT @@max_connections").Scan(&max)
	if err != nil {
		t.Fatalf("%s", err.Error())
	}
	// max = 100
	fmt.Printf("Testing up to %d concurrent connections \r\n", max)
	var remaining, succeeded int32 = int32(max), 0

	var wg sync.WaitGroup
	wg.Add(max)
	var fatalError string
	var once sync.Once
	fatalf := func(s string, vals ...interface{}) {
		once.Do(func() {
			fatalError = fmt.Sprintf(s, vals...)
		})
	}

	for i := 0; i < max; i++ {
		go func(id int) {
			defer wg.Done()

			tx, err := db.Begin()
			atomic.AddInt32(&remaining, -1)
			fmt.Printf("%d ", remaining)

			if err != nil {
				if err.Error() != "Error 1040: Too many connections" {
					//fmt.Printf("Begin: Error on Conn %d: %s", id, err.Error())
					fatalf("Begin: Error on Conn %d: %s", id, err.Error())
					// t.Logf("Begin: Error on Conn %d: %s", id, err.Error())
				}
				//fmt.Printf(" whoops ")
				return
			}

			// keep the connection busy until all connections are open
			for remaining > 0 {
				if _, err = tx.Exec("DO 1"); err != nil {
					//fmt.Printf("Exec: Error on Conn %d: %s", id, err.Error())
					fatalf("Exec: Error on Conn %d: %s", id, err.Error())
					return
				}
			}

			if err = tx.Commit(); err != nil {
				//fmt.Printf("Error on Conn %d: %s", id, err.Error())
				fatalf("Error on Conn %d: %s", id, err.Error())
				return
			}

			// everything went fine with this connection
			atomic.AddInt32(&succeeded, 1)
		}(i)
	}

	fmt.Println("waiting")
	// wait until all conections are open
	wg.Wait()
	fmt.Println("waited")

	if fatalError != "" {
		t.Fatal(fatalError)
	}

	t.Logf("Reached %d concurrent connections\r\n", succeeded)

}
<commit_msg>updated test<commit_after>// Copyright 2014 by tkr@ecix.net (Peering GmbH)
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.

package clustersql

import (
	"database/sql"
	"fmt"
	"github.com/BurntSushi/toml"
	"github.com/go-sql-driver/mysql"
	"os"
	"sync"
	"sync/atomic"
	"testing"
)

var db *sql.DB

type NodeCfg struct {
	Name     string
	HostName string
	Port     int
	UserName string
	Password string
	DBName   string
}

type Config struct {
	Nodes []NodeCfg
}

func TestOpen(t *testing.T) {
	cfgfile := os.Getenv("DBCONFIG")
	if cfgfile == "" {
		cfgfile = "config.toml"
	}
	cfg := new(Config)
	if f, err := os.Open(cfgfile); err != nil {
		t.Fatal(err, "(did you set the DBCONFIG env variable?)")
	} else {
		if _, err := toml.DecodeReader(f, cfg); err != nil {
			t.Fatal(err)
		}
	}

	d := NewDriver("ClusterSql", mysql.MySQLDriver{})

	for _, ncfg := range cfg.Nodes {
		if ncfg.Password != "" {
			d.AddNode(ncfg.Name, fmt.Sprintf("%s:%s@tcp(%s:%d)/%s", ncfg.UserName, ncfg.Password, ncfg.HostName, ncfg.Port, ncfg.DBName))
		} else {
			d.AddNode(ncfg.Name, fmt.Sprintf("%s@tcp(%s:%d)/%s", ncfg.UserName, ncfg.HostName, ncfg.Port, ncfg.DBName))
		}
	}

	sql.Register("cluster", d)
	var err error
	db, err = sql.Open("cluster", "galera")
	if err != nil {
		t.Error(err)
	}
}

func TestDrop(t *testing.T) {
	_, err := db.Exec("DROP TABLE IF EXISTS test")
	if err != nil {
		t.Fatalf(err.Error())
	}
}

func TestCreate(t *testing.T) {
	_, err := db.Exec("CREATE TABLE test (value BOOL)")
	if err != nil {
		t.Fatalf(err.Error())
	}
}

func TestEmptySelect(t *testing.T) {
	rows, err := db.Query("SELECT * FROM test")
	if err != nil {
		t.Fatalf(err.Error())
	}
	if rows.Next() {
		t.Fatalf("unexpected data in empty table")
	}
}

func TestInsert(t *testing.T) {
	res, err := db.Exec("INSERT INTO test VALUES (1)")
	if err != nil {
		t.Fatalf(err.Error())
	}
	count, err := res.RowsAffected()
	if err != nil {
		t.Fatalf("res.RowsAffected() returned error: %s", err.Error())
	}
	if count != 1 {
		t.Fatalf("Expected 1 affected row, got %d", count)
	}
	id, err := res.LastInsertId()
	if err != nil {
		t.Fatalf("res.LastInsertId() returned error: %s", err.Error())
	}
	if id != 0 {
		t.Fatalf("Expected InsertID 0, got %d", id)
	}
}

func TestSelect(t *testing.T) {
	rows, err := db.Query("SELECT value FROM test")
	if err != nil {
		t.Fatalf(err.Error())
	}
	if rows.Next() {
		var out bool
		rows.Scan(&out)
		if true != out {
			t.Errorf("true != %t", out)
		}
		if rows.Next() {
			t.Error("unexpected data")
		}
	} else {
		t.Error("no data")
	}
}

func TestUpdate(t *testing.T) {
	res, err := db.Exec("UPDATE test SET value = ? WHERE value = ?", false, true)
	if err != nil {
		t.Fatalf(err.Error())
	}
	count, err := res.RowsAffected()
	if err != nil {
		t.Fatalf("res.RowsAffected() returned error: %s", err.Error())
	}
	if count != 1 {
		t.Fatalf("Expected 1 affected row, got %d", count)
	}

	rows, err := db.Query("SELECT value FROM test")
	if err != nil {
		t.Fatalf(err.Error())
	}
	if rows.Next() {
		var out bool
		rows.Scan(&out)
		if false != out {
			t.Errorf("false != %t", out)
		}

		if rows.Next() {
			t.Error("unexpected data")
		}
	} else {
		t.Error("no data")
	}
}

func TestDelete(t *testing.T) {
	res, err := db.Exec("DELETE FROM test WHERE value = ?", false)
	if err != nil {
		t.Fatalf(err.Error())
	}
	count, err := res.RowsAffected()
	if err != nil {
		t.Fatalf("res.RowsAffected() returned error: %s", err.Error())
	}
	if count != 1 {
		t.Fatalf("Expected 1 affected row, got %d", count)
	}

	// Check for unexpected rows
	res, err = db.Exec("DELETE FROM test")
	if err != nil {
		t.Fatalf(err.Error())
	}
	count, err = res.RowsAffected()
	if err != nil {
		t.Fatalf("res.RowsAffected() returned error: %s", err.Error())
	}
	if count != 0 {
		t.Fatalf("Expected 0 affected row, got %d", count)
	}
}

func TestConcurrent(t *testing.T) {
	var max int
	err := db.QueryRow("SELECT @@max_connections").Scan(&max)
	if err != nil {
		t.Fatalf("%s", err.Error())
	}
	// max = 100
	fmt.Printf("Testing up to %d concurrent connections \r\n", max)
	var remaining, succeeded int32 = int32(max), 0

	var wg sync.WaitGroup
	wg.Add(max)
	var fatalError string
	var once sync.Once
	fatalf := func(s string, vals ...interface{}) {
		once.Do(func() {
			fatalError = fmt.Sprintf(s, vals...)
		})
	}

	for i := 0; i < max; i++ {
		go func(id int) {
			defer wg.Done()

			tx, err := db.Begin()
			atomic.AddInt32(&remaining, -1)
			fmt.Printf("%d ", remaining)

			if err != nil {
				if err.Error() != "Error 1040: Too many connections" {
					//fmt.Printf("Begin: Error on Conn %d: %s", id, err.Error())
					fatalf("Begin: Error on Conn %d: %s", id, err.Error())
					// t.Logf("Begin: Error on Conn %d: %s", id, err.Error())
				}
				//fmt.Printf(" whoops ")
				return
			}

			// keep the connection busy until all connections are open
			for remaining > 0 {
				if _, err = tx.Exec("DO 1"); err != nil {
					//fmt.Printf("Exec: Error on Conn %d: %s", id, err.Error())
					fatalf("Exec: Error on Conn %d: %s", id, err.Error())
					return
				}
			}

			if err = tx.Commit(); err != nil {
				//fmt.Printf("Error on Conn %d: %s", id, err.Error())
				fatalf("Error on Conn %d: %s", id, err.Error())
				return
			}

			// everything went fine with this connection
			atomic.AddInt32(&succeeded, 1)
		}(i)
	}

	fmt.Println("waiting")
	// wait until all conections are open
	wg.Wait()
	fmt.Println("waited")

	if fatalError != "" {
		t.Fatal(fatalError)
	}

	t.Logf("Reached %d concurrent connections\r\n", succeeded)

}
<|endoftext|>


<commit_before>package cmd

import (
	"bytes"
	"log"
	"os"
	"path/filepath"
	"reflect"
	"testing"

	"github.com/noborus/trdsql"
)

func Test_inputFormat(t *testing.T) {
	type args struct {
		i inputFlag
	}
	tests := []struct {
		name string
		args args
		want trdsql.Format
	}{
		{
			name: "testCSV",
			args: args{
				i: inputFlag{
					CSV: true,
				},
			},
			want: trdsql.CSV,
		},
		{
			name: "testTBLN",
			args: args{
				i: inputFlag{
					TBLN: true,
				},
			},
			want: trdsql.TBLN,
		},
		{
			name: "testGUESS",
			args: args{
				i: inputFlag{},
			},
			want: trdsql.GUESS,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := inputFormat(tt.args.i); !reflect.DeepEqual(got, tt.want) {
				t.Errorf("inputFormat() = %v, want %v", got, tt.want)
			}
		})
	}
}

func Test_outputFormat(t *testing.T) {
	type args struct {
		o outputFlag
	}
	tests := []struct {
		name string
		args args
		want trdsql.Format
	}{
		{
			name: "testCSV",
			args: args{
				o: outputFlag{
					CSV: true,
				},
			},
			want: trdsql.CSV,
		},
		{
			name: "testTBLN",
			args: args{
				o: outputFlag{
					TBLN: true,
				},
			},
			want: trdsql.TBLN,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := outputFormat(tt.args.o); !reflect.DeepEqual(got, tt.want) {
				t.Errorf("outputFormat() = %v, want %v", got, tt.want)
			}
		})
	}
}

func Test_getQuery(t *testing.T) {
	type argss struct {
		args     []string
		fileName string
	}
	tests := []struct {
		name    string
		argss   argss
		want    string
		wantErr bool
	}{
		{
			name: "testARGS",
			argss: argss{
				[]string{"SELECT 1"},
				"",
			},
			want:    "SELECT 1",
			wantErr: false,
		},
		{
			name: "testARGS2",
			argss: argss{
				[]string{"SELECT", "1"},
				"",
			},
			want:    "SELECT 1",
			wantErr: false,
		},
		{
			name: "testFile",
			argss: argss{
				[]string{},
				filepath.Join("..", "testdata", "test.sql"),
			},
			want:    "SELECT * FROM testdata/test.csv\n",
			wantErr: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := getQuery(tt.argss.args, tt.argss.fileName)
			if (err != nil) != tt.wantErr {
				t.Errorf("getQuery() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if got != tt.want {
				t.Errorf("getQuery() = %v, want %v", got, tt.want)
			}
		})
	}
}

func Test_getDB(t *testing.T) {
	type argss struct {
		cfg     *config
		cDB     string
		cDriver string
		cDSN    string
	}
	tests := []struct {
		name  string
		argss argss
		want  string
		want1 string
	}{
		{
			name: "testNoConfig",
			argss: argss{
				cfg:     &config{},
				cDB:     "",
				cDriver: "postgres",
				cDSN:    "dbname=test",
			},
			want:  "postgres",
			want1: "dbname=test",
		},
		{
			name: "testNoConfigDB",
			argss: argss{
				cfg:     &config{},
				cDB:     "test",
				cDriver: "postgres",
				cDSN:    "dbname=\"test\"",
			},
			want:  "postgres",
			want1: "dbname=\"test\"",
		},
		{
			name: "testDSN",
			argss: argss{
				cfg:     &config{},
				cDB:     "",
				cDriver: "",
				cDSN:    "dbname=\"test\"",
			},
			want:  "",
			want1: "dbname=\"test\"",
		},
		{
			name: "testConfig",
			argss: argss{
				cfg: &config{
					Db: "",
					Database: map[string]database{
						"pdb": {
							Driver: "postgres",
							Dsn:    "dbname=\"test\"",
						},
					},
				},
				cDB:     "pdb",
				cDriver: "",
				cDSN:    "",
			},
			want:  "postgres",
			want1: "dbname=\"test\"",
		},
		{
			name: "testConfigErr",
			argss: argss{
				cfg: &config{
					Db: "",
					Database: map[string]database{
						"pdb": {
							Driver: "postgres",
							Dsn:    "dbname=\"test\"",
						},
					},
				},
				cDB:     "sdb",
				cDriver: "",
				cDSN:    "",
			},
			want:  "",
			want1: "",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var buf bytes.Buffer
			log.SetOutput(&buf)
			got, got1 := getDB(tt.argss.cfg, tt.argss.cDB, tt.argss.cDriver, tt.argss.cDSN)
			if got != tt.want {
				t.Errorf("getDB() got = %v, want %v", got, tt.want)
			}
			if got1 != tt.want1 {
				t.Errorf("getDB() got1 = %v, want %v", got1, tt.want1)
			}
		})
	}
}

func Test_optsCommand(t *testing.T) {
	tests := []struct {
		name string
		args []string
		want string
	}{
		{
			name: "testEmpty",
			args: []string{"trdsql", "-a"},
			want: "trdsql",
		},
		{
			name: "testFile",
			args: []string{"trdsql", "-ih", "-a", "test.csv"},
			want: "trdsql -ih",
		},
		{
			name: "testStdin",
			args: []string{"trdsql", "-ih", "-a", "-"},
			want: "trdsql -ih",
		},
		{
			name: "testFile",
			args: []string{"trdsql", "-dsn=\"dbname=test\"", "-a", "test.csv"},
			want: "trdsql -dsn=\"dbname=test\"",
		},
		{
			name: "testDelimiterSpace",
			args: []string{"trdsql", "-id", " ", "-a", "test.csv"},
			want: "trdsql -id \" \"",
		},
		{
			name: "testDelimiterUnder",
			args: []string{"trdsql", "-id", "_", "-a", "test.csv"},
			want: "trdsql -id _",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			opts := trdsql.NewAnalyzeOpts()
			got := optsCommand(opts, tt.args)
			if !reflect.DeepEqual(got.Command, tt.want) {
				t.Errorf("optsCommand() = %v, want %v", got.Command, tt.want)
			}
		})
	}
}

func TestRun(t *testing.T) {
	tests := []struct {
		name string
		args []string
		want int
	}{
		{
			name: "testEmpty",
			args: []string{"trdsql"},
			want: 2,
		},
		{
			name: "testOne",
			args: []string{"trdsql", "SELECT 1"},
			want: 0,
		},
		{
			name: "testErr",
			args: []string{"trdsql", "Err"},
			want: 1,
		},
		{
			name: "testAnalyze",
			args: []string{"trdsql", "-a", filepath.Join("..", "testdata", "test.csv")},
			want: 0,
		},
		{
			name: "testSQLOnly",
			args: []string{"trdsql", "-A", filepath.Join("..", "testdata", "test.csv")},
			want: 0,
		},
		{
			name: "testDBList",
			args: []string{"trdsql", "-dblist"},
			want: 0,
		},
		{
			name: "testHelp",
			args: []string{"trdsql", "-help"},
			want: 2,
		},
		{
			name: "testVersion",
			args: []string{"trdsql", "-version"},
			want: 0,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			outStream, errStream := new(bytes.Buffer), new(bytes.Buffer)
			cli := Cli{
				OutStream: outStream,
				ErrStream: errStream,
			}
			var buf bytes.Buffer
			log.SetOutput(&buf)
			if got := cli.Run(tt.args); got != tt.want {
				t.Errorf("Run() = %v, want %v", got, tt.want)
			}
		})
	}
}

func Test_printDBList(t *testing.T) {
	tests := []struct {
		name string
		cfg  *config
	}{
		{
			name: "test",
			cfg: &config{
				Db: "",
				Database: map[string]database{
					"pdb": database{Driver: "postgres", Dsn: "dbname=test"},
				},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			printDBList(tt.cfg)
		})
	}
}

func Test_colorOpts(t *testing.T) {
	tests := []struct {
		name   string
		setEnv bool
		want   bool
	}{
		{
			name:   "test",
			setEnv: false,
			want:   true,
		},
		{
			name:   "test",
			setEnv: true,
			want:   false,
		},
	}
	for _, tt := range tests {
		opts := trdsql.NewAnalyzeOpts()
		if tt.setEnv {
			os.Setenv("NO_COLOR", "1")
		}
		t.Run(tt.name, func(t *testing.T) {
			if got := colorOpts(opts); !reflect.DeepEqual(got.Color, tt.want) {
				t.Errorf("colorOpts() = %v, want %v", got.Color, tt.want)
			}
		})
	}
}

func Test_quoteOpts(t *testing.T) {
	tests := []struct {
		name   string
		driver string
		want   string
	}{
		{
			name:   "testSQLIte3",
			driver: "sqlite3",
			want:   "\\`",
		},
		{
			name:   "testMySQL",
			driver: "mysql",
			want:   "\\`",
		},
		{
			name:   "testPostgreSQL",
			driver: "postgres",
			want:   `\"`,
		},
	}
	for _, tt := range tests {
		opts := trdsql.NewAnalyzeOpts()
		t.Run(tt.name, func(t *testing.T) {
			got := quoteOpts(opts, tt.driver)
			if !reflect.DeepEqual(got.Quote, tt.want) {
				t.Errorf("quoteOpts() = %v, want %v", got.Quote, tt.want)
			}
		})
	}
}
<commit_msg>exclude windows<commit_after>package cmd

import (
	"bytes"
	"log"
	"os"
	"path/filepath"
	"reflect"
	"runtime"
	"testing"

	"github.com/noborus/trdsql"
)

func Test_inputFormat(t *testing.T) {
	type args struct {
		i inputFlag
	}
	tests := []struct {
		name string
		args args
		want trdsql.Format
	}{
		{
			name: "testCSV",
			args: args{
				i: inputFlag{
					CSV: true,
				},
			},
			want: trdsql.CSV,
		},
		{
			name: "testTBLN",
			args: args{
				i: inputFlag{
					TBLN: true,
				},
			},
			want: trdsql.TBLN,
		},
		{
			name: "testGUESS",
			args: args{
				i: inputFlag{},
			},
			want: trdsql.GUESS,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := inputFormat(tt.args.i); !reflect.DeepEqual(got, tt.want) {
				t.Errorf("inputFormat() = %v, want %v", got, tt.want)
			}
		})
	}
}

func Test_outputFormat(t *testing.T) {
	type args struct {
		o outputFlag
	}
	tests := []struct {
		name string
		args args
		want trdsql.Format
	}{
		{
			name: "testCSV",
			args: args{
				o: outputFlag{
					CSV: true,
				},
			},
			want: trdsql.CSV,
		},
		{
			name: "testTBLN",
			args: args{
				o: outputFlag{
					TBLN: true,
				},
			},
			want: trdsql.TBLN,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := outputFormat(tt.args.o); !reflect.DeepEqual(got, tt.want) {
				t.Errorf("outputFormat() = %v, want %v", got, tt.want)
			}
		})
	}
}

func Test_getQuery(t *testing.T) {
	type argss struct {
		args     []string
		fileName string
	}
	tests := []struct {
		name    string
		argss   argss
		want    string
		wantErr bool
	}{
		{
			name: "testARGS",
			argss: argss{
				[]string{"SELECT 1"},
				"",
			},
			want:    "SELECT 1",
			wantErr: false,
		},
		{
			name: "testARGS2",
			argss: argss{
				[]string{"SELECT", "1"},
				"",
			},
			want:    "SELECT 1",
			wantErr: false,
		},
		{
			name: "testFile",
			argss: argss{
				[]string{},
				filepath.Join("..", "testdata", "test.sql"),
			},
			want:    "SELECT * FROM testdata/test.csv\n",
			wantErr: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := getQuery(tt.argss.args, tt.argss.fileName)
			if (err != nil) != tt.wantErr {
				t.Errorf("getQuery() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if got != tt.want {
				t.Errorf("getQuery() = %v, want %v", got, tt.want)
			}
		})
	}
}

func Test_getDB(t *testing.T) {
	type argss struct {
		cfg     *config
		cDB     string
		cDriver string
		cDSN    string
	}
	tests := []struct {
		name  string
		argss argss
		want  string
		want1 string
	}{
		{
			name: "testNoConfig",
			argss: argss{
				cfg:     &config{},
				cDB:     "",
				cDriver: "postgres",
				cDSN:    "dbname=test",
			},
			want:  "postgres",
			want1: "dbname=test",
		},
		{
			name: "testNoConfigDB",
			argss: argss{
				cfg:     &config{},
				cDB:     "test",
				cDriver: "postgres",
				cDSN:    "dbname=\"test\"",
			},
			want:  "postgres",
			want1: "dbname=\"test\"",
		},
		{
			name: "testDSN",
			argss: argss{
				cfg:     &config{},
				cDB:     "",
				cDriver: "",
				cDSN:    "dbname=\"test\"",
			},
			want:  "",
			want1: "dbname=\"test\"",
		},
		{
			name: "testConfig",
			argss: argss{
				cfg: &config{
					Db: "",
					Database: map[string]database{
						"pdb": {
							Driver: "postgres",
							Dsn:    "dbname=\"test\"",
						},
					},
				},
				cDB:     "pdb",
				cDriver: "",
				cDSN:    "",
			},
			want:  "postgres",
			want1: "dbname=\"test\"",
		},
		{
			name: "testConfigErr",
			argss: argss{
				cfg: &config{
					Db: "",
					Database: map[string]database{
						"pdb": {
							Driver: "postgres",
							Dsn:    "dbname=\"test\"",
						},
					},
				},
				cDB:     "sdb",
				cDriver: "",
				cDSN:    "",
			},
			want:  "",
			want1: "",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var buf bytes.Buffer
			log.SetOutput(&buf)
			got, got1 := getDB(tt.argss.cfg, tt.argss.cDB, tt.argss.cDriver, tt.argss.cDSN)
			if got != tt.want {
				t.Errorf("getDB() got = %v, want %v", got, tt.want)
			}
			if got1 != tt.want1 {
				t.Errorf("getDB() got1 = %v, want %v", got1, tt.want1)
			}
		})
	}
}

func Test_optsCommand(t *testing.T) {
	tests := []struct {
		name string
		args []string
		want string
	}{
		{
			name: "testEmpty",
			args: []string{"trdsql", "-a"},
			want: "trdsql",
		},
		{
			name: "testFile",
			args: []string{"trdsql", "-ih", "-a", "test.csv"},
			want: "trdsql -ih",
		},
		{
			name: "testStdin",
			args: []string{"trdsql", "-ih", "-a", "-"},
			want: "trdsql -ih",
		},
		{
			name: "testFile",
			args: []string{"trdsql", "-dsn=\"dbname=test\"", "-a", "test.csv"},
			want: "trdsql -dsn=\"dbname=test\"",
		},
		{
			name: "testDelimiterSpace",
			args: []string{"trdsql", "-id", " ", "-a", "test.csv"},
			want: "trdsql -id \" \"",
		},
		{
			name: "testDelimiterUnder",
			args: []string{"trdsql", "-id", "_", "-a", "test.csv"},
			want: "trdsql -id _",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			opts := trdsql.NewAnalyzeOpts()
			got := optsCommand(opts, tt.args)
			if !reflect.DeepEqual(got.Command, tt.want) {
				t.Errorf("optsCommand() = %v, want %v", got.Command, tt.want)
			}
		})
	}
}

func TestRun(t *testing.T) {
	tests := []struct {
		name string
		args []string
		want int
	}{
		{
			name: "testEmpty",
			args: []string{"trdsql"},
			want: 2,
		},
		{
			name: "testOne",
			args: []string{"trdsql", "SELECT 1"},
			want: 0,
		},
		{
			name: "testErr",
			args: []string{"trdsql", "Err"},
			want: 1,
		},
		{
			name: "testAnalyze",
			args: []string{"trdsql", "-a", filepath.Join("..", "testdata", "test.csv")},
			want: 0,
		},
		{
			name: "testSQLOnly",
			args: []string{"trdsql", "-A", filepath.Join("..", "testdata", "test.csv")},
			want: 0,
		},
		{
			name: "testDBList",
			args: []string{"trdsql", "-dblist"},
			want: 0,
		},
		{
			name: "testHelp",
			args: []string{"trdsql", "-help"},
			want: 2,
		},
		{
			name: "testVersion",
			args: []string{"trdsql", "-version"},
			want: 0,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			outStream, errStream := new(bytes.Buffer), new(bytes.Buffer)
			cli := Cli{
				OutStream: outStream,
				ErrStream: errStream,
			}
			var buf bytes.Buffer
			log.SetOutput(&buf)
			if got := cli.Run(tt.args); got != tt.want {
				t.Errorf("Run() = %v, want %v", got, tt.want)
			}
		})
	}
}

func Test_printDBList(t *testing.T) {
	tests := []struct {
		name string
		cfg  *config
	}{
		{
			name: "test",
			cfg: &config{
				Db: "",
				Database: map[string]database{
					"pdb": database{Driver: "postgres", Dsn: "dbname=test"},
				},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			printDBList(tt.cfg)
		})
	}
}

func Test_colorOpts(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("skipping specific test")
	}
	tests := []struct {
		name   string
		setEnv bool
		want   bool
	}{
		{
			name:   "test",
			setEnv: false,
			want:   true,
		},
		{
			name:   "test",
			setEnv: true,
			want:   false,
		},
	}
	for _, tt := range tests {
		opts := trdsql.NewAnalyzeOpts()
		if tt.setEnv {
			os.Setenv("NO_COLOR", "1")
		}
		t.Run(tt.name, func(t *testing.T) {
			if got := colorOpts(opts); !reflect.DeepEqual(got.Color, tt.want) {
				t.Errorf("colorOpts() = %v, want %v", got.Color, tt.want)
			}
		})
	}
}

func Test_quoteOpts(t *testing.T) {
	tests := []struct {
		name   string
		driver string
		want   string
	}{
		{
			name:   "testSQLIte3",
			driver: "sqlite3",
			want:   "\\`",
		},
		{
			name:   "testMySQL",
			driver: "mysql",
			want:   "\\`",
		},
		{
			name:   "testPostgreSQL",
			driver: "postgres",
			want:   `\"`,
		},
	}
	for _, tt := range tests {
		opts := trdsql.NewAnalyzeOpts()
		t.Run(tt.name, func(t *testing.T) {
			got := quoteOpts(opts, tt.driver)
			if !reflect.DeepEqual(got.Quote, tt.want) {
				t.Errorf("quoteOpts() = %v, want %v", got.Quote, tt.want)
			}
		})
	}
}
<|endoftext|>


<commit_before>package cmd

import (
	"bytes"
	"context"

	"github.com/mitchellh/cli"
	"github.com/nerdalize/nerd/svc"
	"github.com/pkg/errors"
)

//JobLogs command
type JobLogs struct {
	KubeOpts
	Tail int64 `long:"tail" short:"t" description:"only return the oldest N lines of the process logs"`

	*command
}

//JobLogsFactory creates the command
func JobLogsFactory(ui cli.Ui) cli.CommandFactory {
	cmd := &JobLogs{}
	cmd.command = createCommand(ui, cmd.Execute, cmd.Description, cmd.Usage, cmd)
	return func() (cli.Command, error) {
		return cmd, nil
	}
}

//Execute runs the command
func (cmd *JobLogs) Execute(args []string) (err error) {
	if len(args) < 1 {
		return errors.New(MessageNotEnoughArguments)
	}

	kopts := cmd.KubeOpts
	deps, err := NewDeps(cmd.Logger(), kopts)
	if err != nil {
		return errors.Wrap(err, "failed to configure")
	}

	ctx := context.Background()
	ctx, cancel := context.WithTimeout(ctx, cmd.Timeout)
	defer cancel()

	in := &svc.FetchJobLogsInput{
		Name: args[0],
		Tail: cmd.Tail,
	}

	kube := svc.NewKube(deps, kopts.Namespace)
	out, err := kube.FetchJobLogs(ctx, in)
	if err != nil {
		return renderServiceError(err, "failed to fetch job logs")
	}

	lines := string(bytes.TrimSpace(out.Data))
	if len(lines) < 1 {
		cmd.out.Errorf("No logs visible (anymore) for job '%s'. Maybe the process didn't output any logs or it was created a long time ago: old logs may be discarded", in.Name)
		return nil
	}

	cmd.out.Output(string(out.Data))
	return nil
}

// Description returns long-form help text
func (cmd *JobLogs) Description() string { return cmd.Synopsis() }

// Synopsis returns a one-line
func (cmd *JobLogs) Synopsis() string { return "Return logs for a running job" }

// Usage shows usage
func (cmd *JobLogs) Usage() string { return "nerd job logs [NAME]" }
<commit_msg>add a message when logs are trimmed<commit_after>package cmd

import (
	"bytes"
	"context"

	"github.com/mitchellh/cli"
	"github.com/nerdalize/nerd/pkg/kubevisor"
	"github.com/nerdalize/nerd/svc"
	"github.com/pkg/errors"
)

//JobLogs command
type JobLogs struct {
	KubeOpts
	Tail int64 `long:"tail" short:"t" description:"only return the oldest N lines of the process logs"`

	*command
}

//JobLogsFactory creates the command
func JobLogsFactory(ui cli.Ui) cli.CommandFactory {
	cmd := &JobLogs{}
	cmd.command = createCommand(ui, cmd.Execute, cmd.Description, cmd.Usage, cmd)
	return func() (cli.Command, error) {
		return cmd, nil
	}
}

//Execute runs the command
func (cmd *JobLogs) Execute(args []string) (err error) {
	if len(args) < 1 {
		return errors.New(MessageNotEnoughArguments)
	}

	kopts := cmd.KubeOpts
	deps, err := NewDeps(cmd.Logger(), kopts)
	if err != nil {
		return errors.Wrap(err, "failed to configure")
	}

	ctx := context.Background()
	ctx, cancel := context.WithTimeout(ctx, cmd.Timeout)
	defer cancel()

	in := &svc.FetchJobLogsInput{
		Name: args[0],
		Tail: cmd.Tail,
	}

	kube := svc.NewKube(deps, kopts.Namespace)
	out, err := kube.FetchJobLogs(ctx, in)
	if err != nil {
		return renderServiceError(err, "failed to fetch job logs")
	}

	lines := string(bytes.TrimSpace(out.Data))
	if len(lines) < 1 {
		cmd.out.Info("-- no visible logs returned --")
		return nil
	}

	cmd.out.Output(string(out.Data))
	if int64(len(out.Data)) == kubevisor.MaxLogBytes {
		cmd.out.Info("-- logs are trimmed after this point --")
	}

	return nil
}

// Description returns long-form help text
func (cmd *JobLogs) Description() string { return cmd.Synopsis() }

// Synopsis returns a one-line
func (cmd *JobLogs) Synopsis() string { return "Return logs for a running job" }

// Usage shows usage
func (cmd *JobLogs) Usage() string { return "nerd job logs [NAME]" }
<|endoftext|>


<commit_before>package main

import (
	"bufio"
	"bytes"
	"flag"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"os/exec"
	"runtime/debug"
	"strings"
	"sync"
	"time"

	"git.apache.org/thrift.git/lib/go/thrift"
	"github.com/daviddengcn/go-colortext"
	"github.com/korovkin/limiter"
	"github.com/korovkin/parallel"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promhttp"
)

type logger struct {
	ticket   int
	hostname string
	isError  bool
	buf      *bytes.Buffer
}

var (
	loggerMutex     = new(sync.Mutex)
	loggerIndex     = int(0)
	loggerStartTime = time.Now()
	loggerHostname  = ""
)

var loggerColors = []ct.Color{
	ct.Green,
	ct.Cyan,
	ct.Magenta,
	ct.Yellow,
	ct.Blue,
	ct.Red,
}

func (l *logger) Write(p []byte) (int, error) {
	buf := bytes.NewBuffer(p)
	wrote := 0
	for {
		line, err := buf.ReadBytes('\n')
		if len(line) > 1 {
			now := time.Now().Format("15:01:02")
			s := string(line)
			ts := time.Since(loggerStartTime).String()
			e := "I"
			if l.isError {
				e = "E"
			}

			{
				loggerMutex.Lock()
				ct.ChangeColor(loggerColors[l.ticket%len(loggerColors)], false, ct.None, false)
				fmt.Printf("[%-14s %s %s %d %s] ", ts, l.hostname, now, l.ticket, e)
				ct.ResetColor()
				if l.buf != nil {
					l.buf.Write([]byte(s))
				}
				fmt.Print(s)
				loggerMutex.Unlock()
			}

			wrote += len(line)
		}
		if err != nil {
			break
		}
	}
	if len(p) > 0 && p[len(p)-1] != '\n' {
		fmt.Println()
	}

	return len(p), nil
}

func newLogger(ticket int, collectLines bool) *logger {
	l := &logger{ticket: ticket, buf: nil}
	if collectLines {
		l.buf = &bytes.Buffer{}
	}
	return l
}

func CheckFatal(e error) error {
	if e != nil {
		debug.PrintStack()
		log.Println("CHECK: ERROR:", e)
		panic(e)
	}
	return e
}

func CheckNotFatal(e error) error {
	if e != nil {
		debug.PrintStack()
		log.Println("CHECK: ERROR:", e, e.Error())
	}
	return e
}

func executeCommand(p *Parallel, ticket int, cmdLine string) (*parallel.Output, error) {
	p.StatNumCommandsStart.Inc()
	T_START := time.Now()
	var err error
	output := &parallel.Output{}
	loggerOut := newLogger(ticket, true)
	loggerOut.isError = false
	loggerErr := newLogger(ticket, true)
	loggerErr.isError = true

	defer func() {
		dt := time.Since(T_START)
		fmt.Fprintf(
			loggerOut,
			"execute: done: dt: "+dt.String()+"\n",
		)
		if err == nil {
			p.StatNumCommandsDone.Inc()
			p.StatCommandLatency.Observe(dt.Seconds())
		}
	}()

	// execute remotely:
	if len(p.Slaves) > 0 {
		slave := p.Slaves[ticket%len(p.Slaves)]

		var transport thrift.TTransport
		transport, err = thrift.NewTSocket(slave.Address)
		if err != nil {
			log.Fatalln("failed to dial slave:", err.Error())
		}

		transport, err = p.transportFactory.GetTransport(transport)
		if err != nil {
			log.Fatalln("failed to GetTransport:", err.Error())
		}

		err = transport.Open()
		if err != nil {
			log.Fatalln("failed to open:", err.Error())
		}

		defer transport.Close()
		client := parallel.NewParallelClientFactory(transport, p.protocolFactory)

		cmd := parallel.Cmd{
			CmdLine: cmdLine,
			Ticket:  int64(ticket),
		}

		output, err = client.Execute(&cmd)
		if err != nil {
			log.Fatalln("failed to execute:", err.Error())
		}

		hostname := output.Tags["hostname"]
		loggerOut.hostname = hostname
		loggerErr.hostname = hostname

		fmt.Fprintf(loggerOut,
			"execute: remote: host: %s stdout: [%s]\n",
			hostname,
			output.Stdout)

		fmt.Fprintf(loggerErr,
			"execute: remote: host: %s stderr: [%s]\n",
			hostname,
			output.Stderr)

		return output, err
	}

	// execute locally:
	cs := []string{"/bin/sh", "-c", cmdLine}
	cmd := exec.Command(cs[0], cs[1:]...)
	cmd.Stdin = nil
	cmd.Stdout = loggerOut
	cmd.Stderr = loggerErr
	cmd.Env = append(
		os.Environ(),
		fmt.Sprintf("PARALLEL_TICKER=%d", ticket),
	)

	fmt.Fprintf(loggerOut, "run: '"+cmdLine+"'\n")

	err = cmd.Start()
	if err != nil {
		log.Fatalln("failed to start:", err)
		return output, err
	}

	if err == nil {
		err = cmd.Wait()
	}

	output.Tags = map[string]string{"hostname": loggerHostname}

	if loggerOut.buf != nil {
		output.Stdout = string(loggerOut.buf.Bytes())
	}

	if loggerErr.buf != nil {
		output.Stderr = string(loggerErr.buf.Bytes())
	}

	return output, err
}

type Slave struct {
	Address string
}

type Parallel struct {
	jobs   int
	logger *logger
	worker *limiter.ConcurrencyLimiter

	// master / slave
	protocolFactory  thrift.TProtocolFactory
	transportFactory thrift.TTransportFactory

	// master:
	Slaves []*Slave

	// slave:
	handler         *ParallelSlaveHandler
	serverTransport thrift.TServerTransport
	slaveAddress    string

	// stats:
	StatNumCommandsStart prometheus.Counter
	StatNumCommandsDone  prometheus.Counter
	StatCommandLatency   prometheus.Summary
}

func (p *Parallel) Close() {
	p.worker.Wait()
}

func mainMaster(p *Parallel) {
	var err error

	// connect to slaves:
	for _, slave := range p.Slaves {
		var transport thrift.TTransport
		transport, err = thrift.NewTSocket(slave.Address)
		if err != nil {
			log.Fatalln("failed to dial slave:", err.Error())
		}

		transport, err = p.transportFactory.GetTransport(transport)
		if err != nil {
			log.Fatalln("failed to open:", err.Error())
		}

		err = transport.Open()
		if err != nil {
			log.Fatalln("failed to open:", err.Error())
		}

		defer transport.Close()

		client := parallel.NewParallelClientFactory(transport, p.protocolFactory)
		ok, err := client.Ping()
		if err != nil {
			log.Fatalln("failed to ping client:", err.Error())
		}

		fmt.Fprintf(
			p.logger,
			"adding slave: %s ok: %s",
			slave.Address,
			ok)
	}

	r := bufio.NewReaderSize(os.Stdin, 1*1024*1024)
	fmt.Fprintf(p.logger, "reading from stdin...\n")
	for {
		line, err := r.ReadString('\n')
		if err == io.EOF {
			break
		}
		line = strings.TrimSpace(line)

		p.worker.ExecuteWithTicket(func(ticket int) {
			executeCommand(p, ticket, line)
		})
	}
}

type ParallelSlaveHandler struct {
	p *Parallel
}

func NewParallelSlaveHandler() *ParallelSlaveHandler {
	return &ParallelSlaveHandler{}
}

func (p *ParallelSlaveHandler) Execute(command *parallel.Cmd) (output *parallel.Output, err error) {
	output = nil
	err = nil
	output, err = executeCommand(p.p, int(command.Ticket), command.CmdLine)

	// TODO:: recover, handle panics

	return output, err
}

func (p *ParallelSlaveHandler) Ping() (r string, err error) {
	return "ping:ok", nil
}

func mainSlave(p *Parallel) {
	var err error

	p.serverTransport, err = thrift.NewTServerSocket(p.slaveAddress)

	if err != nil {
		log.Fatalln("failed to start server:", err.Error())
		return
	}

	p.handler = NewParallelSlaveHandler()
	p.handler.p = p

	server := thrift.NewTSimpleServer4(
		parallel.NewParallelProcessor(p.handler),
		p.serverTransport,
		p.transportFactory,
		p.protocolFactory)

	err = server.Serve()
	if err != nil {
		log.Fatalln("failed to run slave:", err.Error())
	}
}

func main() {
	T_START := time.Now()
	logger := newLogger(0, false)
	defer func() {
		fmt.Fprintf(logger, "all done: dt: "+time.Since(T_START).String()+"\n")
	}()

	flag_jobs := flag.Int(
		"j",
		2,
		"num of concurrent jobs")

	flag_slave := flag.Bool(
		"slave",
		false,
		"run as slave")

	flag_slaves := flag.String(
		"slaves",
		"",
		"CSV list of slave addresses")

	flag_slave_address := flag.String(
		"address",
		"localhost:9010",
		"slave address")

	flag_slave_metrics_address := flag.String(
		"metrics_address",
		"localhost:9011",
		"slave metric address")

	loggerHostname, _ = os.Hostname()

	flag.Parse()
	fmt.Fprintf(logger, "concurrency limit: %d", *flag_jobs)
	fmt.Fprintf(logger, "slaves: %s", *flag_slaves)

	p := Parallel{}
	p.jobs = *flag_jobs
	p.logger = logger
	p.worker = limiter.NewConcurrencyLimiter(p.jobs)
	p.slaveAddress = *flag_slave_address
	p.Slaves = []*Slave{}
	for _, slaveAddr := range strings.Split(*flag_slaves, ",") {
		if slaveAddr != "" {
			slave := Slave{Address: slaveAddr}
			p.Slaves = append(p.Slaves, &slave)
		}
	}

	defer p.Close()

	// thrift protocol
	p.protocolFactory = thrift.NewTBinaryProtocolFactoryDefault()

	// thrift transport
	p.transportFactory = thrift.NewTTransportFactory()
	p.transportFactory = thrift.NewTFramedTransportFactory(p.transportFactory)

	// stats:
	p.StatNumCommandsStart = prometheus.NewCounter(
		prometheus.CounterOpts{
			Name: "commands_num_start",
			Help: "num received"})
	err := prometheus.Register(p.StatNumCommandsStart)
	CheckFatal(err)

	p.StatNumCommandsDone = prometheus.NewCounter(
		prometheus.CounterOpts{
			Name: "commands_num_done",
			Help: "num received"})
	err = prometheus.Register(p.StatNumCommandsDone)
	CheckFatal(err)

	p.StatCommandLatency = prometheus.NewSummary(prometheus.SummaryOpts{
		Name: "commands_latency",
		Help: "commands latency stat",
	})
	err = prometheus.Register(p.StatCommandLatency)
	CheckFatal(err)

	if *flag_slave == false {
		loggerHostname = p.slaveAddress
		logger.hostname = loggerHostname

		fmt.Fprintf(logger, "running as master\n")
		mainMaster(&p)
	} else {
		loggerHostname = p.slaveAddress
		logger.hostname = loggerHostname

		// metrics
		metricsHandler := promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{})
		http.HandleFunc("/metrics", func(c http.ResponseWriter, req *http.Request) {
			metricsHandler.ServeHTTP(c, req)
		})

		http.HandleFunc("/",
			func(c http.ResponseWriter, req *http.Request) {
				io.WriteString(c, fmt.Sprintf("parallel %s", time.Now().String()))
			})

		go http.ListenAndServe(*flag_slave_metrics_address, nil)

		fmt.Fprintf(logger, "running as slave on: %s\n", p.slaveAddress)
		mainSlave(&p)
	}
}
<commit_msg>control verbose level<commit_after>package main

import (
	"bufio"
	"bytes"
	"flag"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"os/exec"
	"runtime/debug"
	"strings"
	"sync"
	"time"

	"git.apache.org/thrift.git/lib/go/thrift"
	"github.com/daviddengcn/go-colortext"
	"github.com/korovkin/limiter"
	"github.com/korovkin/parallel"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promhttp"
)

type logger struct {
	ticket   int
	hostname string
	isError  bool
	buf      *bytes.Buffer
	verbose  bool
}

var (
	loggerMutex     = new(sync.Mutex)
	loggerIndex     = int(0)
	loggerStartTime = time.Now()
	loggerHostname  = ""

	flag_verbose = flag.Bool(
		"v",
		false,
		"verbose level: 1")
)

var loggerColors = []ct.Color{
	ct.Green,
	ct.Cyan,
	ct.Magenta,
	ct.Yellow,
	ct.Blue,
	ct.Red,
}

func (l *logger) Write(p []byte) (int, error) {
	buf := bytes.NewBuffer(p)
	wrote := 0
	for {
		line, err := buf.ReadBytes('\n')
		if len(line) > 1 {
			now := time.Now().Format("15:01:02")
			s := string(line)
			ts := time.Since(loggerStartTime).String()
			e := "I"
			if l.isError {
				e = "E"
			}

			if l.verbose {
				loggerMutex.Lock()
				ct.ChangeColor(loggerColors[l.ticket%len(loggerColors)], false, ct.None, false)
				fmt.Printf("[%-14s %s %s %d %s] ", ts, l.hostname, now, l.ticket, e)
				ct.ResetColor()
				if l.buf != nil {
					l.buf.Write([]byte(s))
				}
				fmt.Print(s)
				loggerMutex.Unlock()
			}

			wrote += len(line)
		}
		if err != nil {
			break
		}
	}
	if len(p) > 0 && p[len(p)-1] != '\n' {
		fmt.Println()
	}

	return len(p), nil
}

func newLogger(ticket int, collectLines bool) *logger {
	l := &logger{ticket: ticket, buf: nil}
	if collectLines {
		l.buf = &bytes.Buffer{}
	}
	l.verbose = true
	return l
}

func CheckFatal(e error) error {
	if e != nil {
		debug.PrintStack()
		log.Println("CHECK: ERROR:", e)
		panic(e)
	}
	return e
}

func CheckNotFatal(e error) error {
	if e != nil {
		debug.PrintStack()
		log.Println("CHECK: ERROR:", e, e.Error())
	}
	return e
}

func executeCommand(p *Parallel, ticket int, cmdLine string) (*parallel.Output, error) {
	p.StatNumCommandsStart.Inc()
	T_START := time.Now()
	var err error
	output := &parallel.Output{}
	loggerOut := newLogger(ticket, true)
	loggerOut.isError = false
	loggerErr := newLogger(ticket, true)
	loggerErr.isError = true

	defer func() {
		dt := time.Since(T_START)
		fmt.Fprintf(
			loggerOut,
			"execute: done: dt: "+dt.String()+"\n",
		)
		if err == nil {
			p.StatNumCommandsDone.Inc()
			p.StatCommandLatency.Observe(dt.Seconds())
		}
	}()

	// execute remotely:
	if len(p.Slaves) > 0 {
		slave := p.Slaves[ticket%len(p.Slaves)]

		var transport thrift.TTransport
		transport, err = thrift.NewTSocket(slave.Address)
		if err != nil {
			log.Fatalln("failed to dial slave:", err.Error())
		}

		transport, err = p.transportFactory.GetTransport(transport)
		if err != nil {
			log.Fatalln("failed to GetTransport:", err.Error())
		}

		err = transport.Open()
		if err != nil {
			log.Fatalln("failed to open:", err.Error())
		}

		defer transport.Close()
		client := parallel.NewParallelClientFactory(transport, p.protocolFactory)

		cmd := parallel.Cmd{
			CmdLine: cmdLine,
			Ticket:  int64(ticket),
		}

		output, err = client.Execute(&cmd)
		if err != nil {
			log.Fatalln("failed to execute:", err.Error())
		}

		hostname := output.Tags["hostname"]
		loggerOut.hostname = hostname
		loggerErr.hostname = hostname

		if *flag_verbose {
			fmt.Fprintf(loggerOut,
				"execute: remote: host: %s stdout: [%s]\n",
				hostname,
				output.Stdout)

			fmt.Fprintf(loggerErr,
				"execute: remote: host: %s stderr: [%s]\n",
				hostname,
				output.Stderr)
		}

		return output, err
	}

	// execute locally:
	cs := []string{"/bin/sh", "-c", cmdLine}
	cmd := exec.Command(cs[0], cs[1:]...)
	cmd.Stdin = nil
	cmd.Stdout = loggerOut
	cmd.Stderr = loggerErr
	cmd.Env = append(
		os.Environ(),
		fmt.Sprintf("PARALLEL_TICKER=%d", ticket),
	)

	fmt.Fprintf(loggerOut, "start: '"+cmdLine+"'\n")

	loggerOut.verbose = *flag_verbose
	loggerErr.verbose = *flag_verbose

	err = cmd.Start()
	if err != nil {
		log.Fatalln("failed to start:", err)
		return output, err
	}

	if err == nil {
		err = cmd.Wait()
	}

	loggerOut.verbose = true
	loggerErr.verbose = true

	output.Tags = map[string]string{"hostname": loggerHostname}

	if loggerOut.buf != nil {
		output.Stdout = string(loggerOut.buf.Bytes())
	}

	if loggerErr.buf != nil {
		output.Stderr = string(loggerErr.buf.Bytes())
	}

	return output, err
}

type Slave struct {
	Address string
}

type Parallel struct {
	jobs   int
	logger *logger
	worker *limiter.ConcurrencyLimiter

	// master / slave
	protocolFactory  thrift.TProtocolFactory
	transportFactory thrift.TTransportFactory

	// master:
	Slaves []*Slave

	// slave:
	handler         *ParallelSlaveHandler
	serverTransport thrift.TServerTransport
	slaveAddress    string

	// stats:
	StatNumCommandsStart prometheus.Counter
	StatNumCommandsDone  prometheus.Counter
	StatCommandLatency   prometheus.Summary
}

func (p *Parallel) Close() {
	p.worker.Wait()
}

func mainMaster(p *Parallel) {
	var err error

	// connect to slaves:
	for _, slave := range p.Slaves {
		var transport thrift.TTransport
		transport, err = thrift.NewTSocket(slave.Address)
		if err != nil {
			log.Fatalln("failed to dial slave:", err.Error())
		}

		transport, err = p.transportFactory.GetTransport(transport)
		if err != nil {
			log.Fatalln("failed to open:", err.Error())
		}

		err = transport.Open()
		if err != nil {
			log.Fatalln("failed to open:", err.Error())
		}

		defer transport.Close()

		client := parallel.NewParallelClientFactory(transport, p.protocolFactory)
		ok, err := client.Ping()
		if err != nil {
			log.Fatalln("failed to ping client:", err.Error())
		}

		fmt.Fprintf(
			p.logger,
			"adding slave: %s ok: %s",
			slave.Address,
			ok)
	}

	r := bufio.NewReaderSize(os.Stdin, 1*1024*1024)
	fmt.Fprintf(p.logger, "reading from stdin...\n")
	for {
		line, err := r.ReadString('\n')
		if err == io.EOF {
			break
		}
		line = strings.TrimSpace(line)

		p.worker.ExecuteWithTicket(func(ticket int) {
			executeCommand(p, ticket, line)
		})
	}
}

type ParallelSlaveHandler struct {
	p *Parallel
}

func NewParallelSlaveHandler() *ParallelSlaveHandler {
	return &ParallelSlaveHandler{}
}

func (p *ParallelSlaveHandler) Execute(command *parallel.Cmd) (output *parallel.Output, err error) {
	output = nil
	err = nil
	output, err = executeCommand(p.p, int(command.Ticket), command.CmdLine)

	// TODO:: recover, handle panics

	return output, err
}

func (p *ParallelSlaveHandler) Ping() (r string, err error) {
	return "ping:ok", nil
}

func mainSlave(p *Parallel) {
	var err error

	p.serverTransport, err = thrift.NewTServerSocket(p.slaveAddress)

	if err != nil {
		log.Fatalln("failed to start server:", err.Error())
		return
	}

	p.handler = NewParallelSlaveHandler()
	p.handler.p = p

	server := thrift.NewTSimpleServer4(
		parallel.NewParallelProcessor(p.handler),
		p.serverTransport,
		p.transportFactory,
		p.protocolFactory)

	err = server.Serve()
	if err != nil {
		log.Fatalln("failed to run slave:", err.Error())
	}
}

func main() {
	T_START := time.Now()
	logger := newLogger(0, false)
	defer func() {
		fmt.Fprintf(logger, "all done: dt: "+time.Since(T_START).String()+"\n")
	}()

	flag_jobs := flag.Int(
		"j",
		2,
		"num of concurrent jobs")

	flag_slave := flag.Bool(
		"slave",
		false,
		"run as slave")

	flag_slaves := flag.String(
		"slaves",
		"",
		"CSV list of slave addresses")

	flag_slave_address := flag.String(
		"address",
		"localhost:9010",
		"slave address")

	flag_slave_metrics_address := flag.String(
		"metrics_address",
		"localhost:9011",
		"slave metric address")

	loggerHostname, _ = os.Hostname()

	flag.Parse()
	fmt.Fprintf(logger, "concurrency limit: %d", *flag_jobs)
	fmt.Fprintf(logger, "slaves: %s", *flag_slaves)

	p := Parallel{}
	p.jobs = *flag_jobs
	p.logger = logger
	p.worker = limiter.NewConcurrencyLimiter(p.jobs)
	p.slaveAddress = *flag_slave_address
	p.Slaves = []*Slave{}
	for _, slaveAddr := range strings.Split(*flag_slaves, ",") {
		if slaveAddr != "" {
			slave := Slave{Address: slaveAddr}
			p.Slaves = append(p.Slaves, &slave)
		}
	}

	defer p.Close()

	// thrift protocol
	p.protocolFactory = thrift.NewTBinaryProtocolFactoryDefault()

	// thrift transport
	p.transportFactory = thrift.NewTTransportFactory()
	p.transportFactory = thrift.NewTFramedTransportFactory(p.transportFactory)

	// stats:
	p.StatNumCommandsStart = prometheus.NewCounter(
		prometheus.CounterOpts{
			Name: "commands_num_start",
			Help: "num received"})
	err := prometheus.Register(p.StatNumCommandsStart)
	CheckFatal(err)

	p.StatNumCommandsDone = prometheus.NewCounter(
		prometheus.CounterOpts{
			Name: "commands_num_done",
			Help: "num received"})
	err = prometheus.Register(p.StatNumCommandsDone)
	CheckFatal(err)

	p.StatCommandLatency = prometheus.NewSummary(prometheus.SummaryOpts{
		Name: "commands_latency",
		Help: "commands latency stat",
	})
	err = prometheus.Register(p.StatCommandLatency)
	CheckFatal(err)

	if *flag_slave == false {
		loggerHostname = p.slaveAddress
		logger.hostname = loggerHostname

		fmt.Fprintf(logger, "running as master\n")
		mainMaster(&p)
	} else {
		loggerHostname = p.slaveAddress
		logger.hostname = loggerHostname

		// metrics
		metricsHandler := promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{})
		http.HandleFunc("/metrics", func(c http.ResponseWriter, req *http.Request) {
			metricsHandler.ServeHTTP(c, req)
		})

		http.HandleFunc("/",
			func(c http.ResponseWriter, req *http.Request) {
				io.WriteString(c, fmt.Sprintf("parallel %s", time.Now().String()))
			})

		go http.ListenAndServe(*flag_slave_metrics_address, nil)

		fmt.Fprintf(logger, "running as slave on: %s\n", p.slaveAddress)
		mainSlave(&p)
	}
}
<|endoftext|>


<commit_before>// Show the dynamic progress bar

package cmd

import (
	"bytes"
	"fmt"
	"os"
	"strings"
	"sync"
	"time"

	"github.com/rclone/rclone/fs"
	"github.com/rclone/rclone/fs/accounting"
	"github.com/rclone/rclone/fs/log"
	"golang.org/x/crypto/ssh/terminal"
)

const (
	// interval between progress prints
	defaultProgressInterval = 500 * time.Millisecond
	// time format for logging
	logTimeFormat = "2006-01-02 15:04:05"
)

// startProgress starts the progress bar printing
//
// It returns a func which should be called to stop the stats.
func startProgress() func() {
	err := initTerminal()
	if err != nil {
		fs.Errorf(nil, "Failed to start progress: %v", err)
		return func() {}
	}
	stopStats := make(chan struct{})
	oldLogPrint := fs.LogPrint
	if !log.Redirected() {
		// Intercept the log calls if not logging to file or syslog
		fs.LogPrint = func(level fs.LogLevel, text string) {
			printProgress(fmt.Sprintf("%s %-6s: %s", time.Now().Format(logTimeFormat), level, text))

		}
	}
	var wg sync.WaitGroup
	wg.Add(1)
	go func() {
		defer wg.Done()
		progressInterval := defaultProgressInterval
		if ShowStats() && *statsInterval > 0 {
			progressInterval = *statsInterval
		}
		ticker := time.NewTicker(progressInterval)
		for {
			select {
			case <-ticker.C:
				printProgress("")
			case <-stopStats:
				ticker.Stop()
				printProgress("")
				fs.LogPrint = oldLogPrint
				fmt.Println("")
				return
			}
		}
	}()
	return func() {
		close(stopStats)
		wg.Wait()
	}
}

// VT100 codes
const (
	eraseLine         = "\x1b[2K"
	moveToStartOfLine = "\x1b[0G"
	moveUp            = "\x1b[A"
)

// state for the progress printing
var (
	nlines     = 0 // number of lines in the previous stats block
	progressMu sync.Mutex
)

// printProgress prints the progress with an optional log
func printProgress(logMessage string) {
	var buf bytes.Buffer
	w, h, err := terminal.GetSize(int(os.Stdout.Fd()))
	if err != nil {
		w, h = 80, 25
	}
	_ = h
	stats := strings.TrimSpace(accounting.GlobalStats().String())
	logMessage = strings.TrimSpace(logMessage)

	out := func(s string) {
		buf.WriteString(s)
	}

	// Lock from here to prevent terminal corruption
	progressMu.Lock()
	progressMu.Unlock()

	if logMessage != "" {
		out("\n")
		out(moveUp)
	}
	// Move to the start of the block we wrote erasing all the previous lines
	for i := 0; i < nlines-1; i++ {
		out(eraseLine)
		out(moveUp)
	}
	out(eraseLine)
	out(moveToStartOfLine)
	if logMessage != "" {
		out(eraseLine)
		out(logMessage + "\n")
	}
	fixedLines := strings.Split(stats, "\n")
	nlines = len(fixedLines)
	for i, line := range fixedLines {
		if len(line) > w {
			line = line[:w]
		}
		out(line)
		if i != nlines-1 {
			out("\n")
		}
	}
	writeToTerminal(buf.Bytes())
}
<commit_msg>Revert "cmd: shorten the locking window when using --progress to avoid deadlock"<commit_after>// Show the dynamic progress bar

package cmd

import (
	"bytes"
	"fmt"
	"os"
	"strings"
	"sync"
	"time"

	"github.com/rclone/rclone/fs"
	"github.com/rclone/rclone/fs/accounting"
	"github.com/rclone/rclone/fs/log"
	"golang.org/x/crypto/ssh/terminal"
)

const (
	// interval between progress prints
	defaultProgressInterval = 500 * time.Millisecond
	// time format for logging
	logTimeFormat = "2006-01-02 15:04:05"
)

// startProgress starts the progress bar printing
//
// It returns a func which should be called to stop the stats.
func startProgress() func() {
	err := initTerminal()
	if err != nil {
		fs.Errorf(nil, "Failed to start progress: %v", err)
		return func() {}
	}
	stopStats := make(chan struct{})
	oldLogPrint := fs.LogPrint
	if !log.Redirected() {
		// Intercept the log calls if not logging to file or syslog
		fs.LogPrint = func(level fs.LogLevel, text string) {
			printProgress(fmt.Sprintf("%s %-6s: %s", time.Now().Format(logTimeFormat), level, text))

		}
	}
	var wg sync.WaitGroup
	wg.Add(1)
	go func() {
		defer wg.Done()
		progressInterval := defaultProgressInterval
		if ShowStats() && *statsInterval > 0 {
			progressInterval = *statsInterval
		}
		ticker := time.NewTicker(progressInterval)
		for {
			select {
			case <-ticker.C:
				printProgress("")
			case <-stopStats:
				ticker.Stop()
				printProgress("")
				fs.LogPrint = oldLogPrint
				fmt.Println("")
				return
			}
		}
	}()
	return func() {
		close(stopStats)
		wg.Wait()
	}
}

// VT100 codes
const (
	eraseLine         = "\x1b[2K"
	moveToStartOfLine = "\x1b[0G"
	moveUp            = "\x1b[A"
)

// state for the progress printing
var (
	nlines     = 0 // number of lines in the previous stats block
	progressMu sync.Mutex
)

// printProgress prints the progress with an optional log
func printProgress(logMessage string) {
	progressMu.Lock()
	defer progressMu.Unlock()

	var buf bytes.Buffer
	w, h, err := terminal.GetSize(int(os.Stdout.Fd()))
	if err != nil {
		w, h = 80, 25
	}
	_ = h
	stats := strings.TrimSpace(accounting.GlobalStats().String())
	logMessage = strings.TrimSpace(logMessage)

	out := func(s string) {
		buf.WriteString(s)
	}

	if logMessage != "" {
		out("\n")
		out(moveUp)
	}
	// Move to the start of the block we wrote erasing all the previous lines
	for i := 0; i < nlines-1; i++ {
		out(eraseLine)
		out(moveUp)
	}
	out(eraseLine)
	out(moveToStartOfLine)
	if logMessage != "" {
		out(eraseLine)
		out(logMessage + "\n")
	}
	fixedLines := strings.Split(stats, "\n")
	nlines = len(fixedLines)
	for i, line := range fixedLines {
		if len(line) > w {
			line = line[:w]
		}
		out(line)
		if i != nlines-1 {
			out("\n")
		}
	}
	writeToTerminal(buf.Bytes())
}
<|endoftext|>