<commit_before>package main

import (
	"context"
	"encoding/json"
	"io"
	"sort"
	"strings"

	"github.com/restic/restic/internal/errors"
	"github.com/restic/restic/internal/restic"
	"github.com/spf13/cobra"
)

var cmdForget = &cobra.Command{
	Use:   "forget [flags] [snapshot ID] [...]",
	Short: "Remove snapshots from the repository",
	Long: `
The "forget" command removes snapshots according to a policy. Please note that
this command really only deletes the snapshot object in the repository, which
is a reference to data stored there. In order to remove this (now unreferenced)
data after 'forget' was run successfully, see the 'prune' command. `,
	DisableAutoGenTag: true,
	RunE: func(cmd *cobra.Command, args []string) error {
		return runForget(forgetOptions, globalOptions, args)
	},
}

// ForgetOptions collects all options for the forget command.
type ForgetOptions struct {
	Last     int
	Hourly   int
	Daily    int
	Weekly   int
	Monthly  int
	Yearly   int
	Within   restic.Duration
	KeepTags restic.TagLists

	Host    string
	Tags    restic.TagLists
	Paths   []string
	Compact bool

	// Grouping
	GroupBy string
	DryRun  bool
	Prune   bool
}

var forgetOptions ForgetOptions

func init() {
	cmdRoot.AddCommand(cmdForget)

	f := cmdForget.Flags()
	f.IntVarP(&forgetOptions.Last, "keep-last", "l", 0, "keep the last `n` snapshots")
	f.IntVarP(&forgetOptions.Hourly, "keep-hourly", "H", 0, "keep the last `n` hourly snapshots")
	f.IntVarP(&forgetOptions.Daily, "keep-daily", "d", 0, "keep the last `n` daily snapshots")
	f.IntVarP(&forgetOptions.Weekly, "keep-weekly", "w", 0, "keep the last `n` weekly snapshots")
	f.IntVarP(&forgetOptions.Monthly, "keep-monthly", "m", 0, "keep the last `n` monthly snapshots")
	f.IntVarP(&forgetOptions.Yearly, "keep-yearly", "y", 0, "keep the last `n` yearly snapshots")
	f.VarP(&forgetOptions.Within, "keep-within", "", "keep snapshots that are newer than `duration` (eg. 1y5m7d2h) relative to the latest snapshot")

	f.Var(&forgetOptions.KeepTags, "keep-tag", "keep snapshots with this `taglist` (can be specified multiple times)")
	f.StringVar(&forgetOptions.Host, "host", "", "only consider snapshots with the given `host`")
	f.StringVar(&forgetOptions.Host, "hostname", "", "only consider snapshots with the given `hostname`")
	f.MarkDeprecated("hostname", "use --host")

	f.Var(&forgetOptions.Tags, "tag", "only consider snapshots which include this `taglist` in the format `tag[,tag,...]` (can be specified multiple times)")

	f.StringArrayVar(&forgetOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path` (can be specified multiple times)")
	f.BoolVarP(&forgetOptions.Compact, "compact", "c", false, "use compact format")

	f.StringVarP(&forgetOptions.GroupBy, "group-by", "g", "host,paths", "string for grouping snapshots by host,paths,tags")
	f.BoolVarP(&forgetOptions.DryRun, "dry-run", "n", false, "do not delete anything, just print what would be done")
	f.BoolVar(&forgetOptions.Prune, "prune", false, "automatically run the 'prune' command if snapshots have been removed")

	f.SortFlags = false
}

func runForget(opts ForgetOptions, gopts GlobalOptions, args []string) error {
	repo, err := OpenRepository(gopts)
	if err != nil {
		return err
	}

	lock, err := lockRepoExclusive(repo)
	defer unlockRepo(lock)
	if err != nil {
		return err
	}

	// group by hostname and dirs
	type key struct {
		Hostname string
		Paths    []string
		Tags     []string
	}
	snapshotGroups := make(map[string]restic.Snapshots)

	var GroupByTag bool
	var GroupByHost bool
	var GroupByPath bool
	var GroupOptionList []string

	GroupOptionList = strings.Split(opts.GroupBy, ",")

	for _, option := range GroupOptionList {
		switch option {
		case "host":
			GroupByHost = true
		case "paths":
			GroupByPath = true
		case "tags":
			GroupByTag = true
		case "":
		default:
			return errors.Fatal("unknown grouping option: '" + option + "'")
		}
	}

	removeSnapshots := 0

	ctx, cancel := context.WithCancel(gopts.ctx)
	defer cancel()
	for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, args) {
		if len(args) > 0 {
			// When explicit snapshots args are given, remove them immediately.
			if !opts.DryRun {
				h := restic.Handle{Type: restic.SnapshotFile, Name: sn.ID().String()}
				if err = repo.Backend().Remove(gopts.ctx, h); err != nil {
					return err
				}
				Verbosef("removed snapshot %v\n", sn.ID().Str())
				removeSnapshots++
			} else {
				Verbosef("would have removed snapshot %v\n", sn.ID().Str())
			}
		} else {
			// Determining grouping-keys
			var tags []string
			var hostname string
			var paths []string

			if GroupByTag {
				tags = sn.Tags
				sort.StringSlice(tags).Sort()
			}
			if GroupByHost {
				hostname = sn.Hostname
			}
			if GroupByPath {
				paths = sn.Paths
			}

			sort.StringSlice(sn.Paths).Sort()
			var k []byte
			var err error

			k, err = json.Marshal(key{Tags: tags, Hostname: hostname, Paths: paths})

			if err != nil {
				return err
			}
			snapshotGroups[string(k)] = append(snapshotGroups[string(k)], sn)
		}
	}

	policy := restic.ExpirePolicy{
		Last:    opts.Last,
		Hourly:  opts.Hourly,
		Daily:   opts.Daily,
		Weekly:  opts.Weekly,
		Monthly: opts.Monthly,
		Yearly:  opts.Yearly,
		Within:  opts.Within,
		Tags:    opts.KeepTags,
	}

	if policy.Empty() && len(args) == 0 {
		Verbosef("no policy was specified, no snapshots will be removed\n")
	}

	if !policy.Empty() {
		if !gopts.JSON {
			Verbosef("Applying Policy: %v\n", policy)
		}

		var jsonGroups []*ForgetGroup

		for k, snapshotGroup := range snapshotGroups {
			var key key
			if json.Unmarshal([]byte(k), &key) != nil {
				return err
			}

			var fg ForgetGroup
			// Info
			if !gopts.JSON {
				Verbosef("snapshots")
			}
			var infoStrings []string
			if GroupByTag {
				infoStrings = append(infoStrings, "tags ["+strings.Join(key.Tags, ", ")+"]")
				fg.Tags = key.Tags
			}
			if GroupByHost {
				infoStrings = append(infoStrings, "host ["+key.Hostname+"]")
				fg.Host = key.Hostname
			}
			if GroupByPath {
				infoStrings = append(infoStrings, "paths ["+strings.Join(key.Paths, ", ")+"]")
				fg.Paths = key.Paths
			}
			if infoStrings != nil && !gopts.JSON {
				Verbosef(" for (" + strings.Join(infoStrings, ", ") + ")")
			}
			if !gopts.JSON {
				Verbosef(":\n\n")
			}

			keep, remove, reasons := restic.ApplyPolicy(snapshotGroup, policy)

			if len(keep) != 0 && !gopts.Quiet && !gopts.JSON {
				Printf("keep %d snapshots:\n", len(keep))
				PrintSnapshots(globalOptions.stdout, keep, reasons, opts.Compact)
				Printf("\n")
			}
			addJSONSnapshots(&fg.Keep, keep)

			if len(remove) != 0 && !gopts.Quiet && !gopts.JSON {
				Printf("remove %d snapshots:\n", len(remove))
				PrintSnapshots(globalOptions.stdout, remove, nil, opts.Compact)
				Printf("\n")
			}
			addJSONSnapshots(&fg.Remove, remove)

			jsonGroups = append(jsonGroups, &fg)

			removeSnapshots += len(remove)

			if !opts.DryRun {
				for _, sn := range remove {
					h := restic.Handle{Type: restic.SnapshotFile, Name: sn.ID().String()}
					err = repo.Backend().Remove(gopts.ctx, h)
					if err != nil {
						return err
					}
				}
			}
		}

		if gopts.JSON {
			err = printJSONForget(gopts.stdout, jsonGroups)
			if err != nil {
				return err
			}
		}
	}

	if removeSnapshots > 0 && opts.Prune {
		Verbosef("%d snapshots have been removed, running prune\n", removeSnapshots)
		if !opts.DryRun {
			return pruneRepository(gopts, repo)
		}
	}

	return nil
}

// ForgetGroup helps to print what is forgotten in JSON.
type ForgetGroup struct {
	Tags   []string   `json:"tags"`
	Host   string     `json:"host"`
	Paths  []string   `json:"paths"`
	Keep   []Snapshot `json:"keep"`
	Remove []Snapshot `json:"remove"`
}

func addJSONSnapshots(js *[]Snapshot, list restic.Snapshots) {
	for _, sn := range list {
		k := Snapshot{
			Snapshot: sn,
			ID:       sn.ID(),
			ShortID:  sn.ID().Str(),
		}
		*js = append(*js, k)
	}
}

func printJSONForget(stdout io.Writer, forgets []*ForgetGroup) error {
	return json.NewEncoder(stdout).Encode(forgets)
}
<commit_msg>Include reasons in json output of forget<commit_after>package main

import (
	"context"
	"encoding/json"
	"io"
	"sort"
	"strings"

	"github.com/restic/restic/internal/errors"
	"github.com/restic/restic/internal/restic"
	"github.com/spf13/cobra"
)

var cmdForget = &cobra.Command{
	Use:   "forget [flags] [snapshot ID] [...]",
	Short: "Remove snapshots from the repository",
	Long: `
The "forget" command removes snapshots according to a policy. Please note that
this command really only deletes the snapshot object in the repository, which
is a reference to data stored there. In order to remove this (now unreferenced)
data after 'forget' was run successfully, see the 'prune' command. `,
	DisableAutoGenTag: true,
	RunE: func(cmd *cobra.Command, args []string) error {
		return runForget(forgetOptions, globalOptions, args)
	},
}

// ForgetOptions collects all options for the forget command.
type ForgetOptions struct {
	Last     int
	Hourly   int
	Daily    int
	Weekly   int
	Monthly  int
	Yearly   int
	Within   restic.Duration
	KeepTags restic.TagLists

	Host    string
	Tags    restic.TagLists
	Paths   []string
	Compact bool

	// Grouping
	GroupBy string
	DryRun  bool
	Prune   bool
}

var forgetOptions ForgetOptions

func init() {
	cmdRoot.AddCommand(cmdForget)

	f := cmdForget.Flags()
	f.IntVarP(&forgetOptions.Last, "keep-last", "l", 0, "keep the last `n` snapshots")
	f.IntVarP(&forgetOptions.Hourly, "keep-hourly", "H", 0, "keep the last `n` hourly snapshots")
	f.IntVarP(&forgetOptions.Daily, "keep-daily", "d", 0, "keep the last `n` daily snapshots")
	f.IntVarP(&forgetOptions.Weekly, "keep-weekly", "w", 0, "keep the last `n` weekly snapshots")
	f.IntVarP(&forgetOptions.Monthly, "keep-monthly", "m", 0, "keep the last `n` monthly snapshots")
	f.IntVarP(&forgetOptions.Yearly, "keep-yearly", "y", 0, "keep the last `n` yearly snapshots")
	f.VarP(&forgetOptions.Within, "keep-within", "", "keep snapshots that are newer than `duration` (eg. 1y5m7d2h) relative to the latest snapshot")

	f.Var(&forgetOptions.KeepTags, "keep-tag", "keep snapshots with this `taglist` (can be specified multiple times)")
	f.StringVar(&forgetOptions.Host, "host", "", "only consider snapshots with the given `host`")
	f.StringVar(&forgetOptions.Host, "hostname", "", "only consider snapshots with the given `hostname`")
	f.MarkDeprecated("hostname", "use --host")

	f.Var(&forgetOptions.Tags, "tag", "only consider snapshots which include this `taglist` in the format `tag[,tag,...]` (can be specified multiple times)")

	f.StringArrayVar(&forgetOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path` (can be specified multiple times)")
	f.BoolVarP(&forgetOptions.Compact, "compact", "c", false, "use compact format")

	f.StringVarP(&forgetOptions.GroupBy, "group-by", "g", "host,paths", "string for grouping snapshots by host,paths,tags")
	f.BoolVarP(&forgetOptions.DryRun, "dry-run", "n", false, "do not delete anything, just print what would be done")
	f.BoolVar(&forgetOptions.Prune, "prune", false, "automatically run the 'prune' command if snapshots have been removed")

	f.SortFlags = false
}

func runForget(opts ForgetOptions, gopts GlobalOptions, args []string) error {
	repo, err := OpenRepository(gopts)
	if err != nil {
		return err
	}

	lock, err := lockRepoExclusive(repo)
	defer unlockRepo(lock)
	if err != nil {
		return err
	}

	// group by hostname and dirs
	type key struct {
		Hostname string
		Paths    []string
		Tags     []string
	}
	snapshotGroups := make(map[string]restic.Snapshots)

	var GroupByTag bool
	var GroupByHost bool
	var GroupByPath bool
	var GroupOptionList []string

	GroupOptionList = strings.Split(opts.GroupBy, ",")

	for _, option := range GroupOptionList {
		switch option {
		case "host":
			GroupByHost = true
		case "paths":
			GroupByPath = true
		case "tags":
			GroupByTag = true
		case "":
		default:
			return errors.Fatal("unknown grouping option: '" + option + "'")
		}
	}

	removeSnapshots := 0

	ctx, cancel := context.WithCancel(gopts.ctx)
	defer cancel()
	for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, args) {
		if len(args) > 0 {
			// When explicit snapshots args are given, remove them immediately.
			if !opts.DryRun {
				h := restic.Handle{Type: restic.SnapshotFile, Name: sn.ID().String()}
				if err = repo.Backend().Remove(gopts.ctx, h); err != nil {
					return err
				}
				Verbosef("removed snapshot %v\n", sn.ID().Str())
				removeSnapshots++
			} else {
				Verbosef("would have removed snapshot %v\n", sn.ID().Str())
			}
		} else {
			// Determining grouping-keys
			var tags []string
			var hostname string
			var paths []string

			if GroupByTag {
				tags = sn.Tags
				sort.StringSlice(tags).Sort()
			}
			if GroupByHost {
				hostname = sn.Hostname
			}
			if GroupByPath {
				paths = sn.Paths
			}

			sort.StringSlice(sn.Paths).Sort()
			var k []byte
			var err error

			k, err = json.Marshal(key{Tags: tags, Hostname: hostname, Paths: paths})

			if err != nil {
				return err
			}
			snapshotGroups[string(k)] = append(snapshotGroups[string(k)], sn)
		}
	}

	policy := restic.ExpirePolicy{
		Last:    opts.Last,
		Hourly:  opts.Hourly,
		Daily:   opts.Daily,
		Weekly:  opts.Weekly,
		Monthly: opts.Monthly,
		Yearly:  opts.Yearly,
		Within:  opts.Within,
		Tags:    opts.KeepTags,
	}

	if policy.Empty() && len(args) == 0 {
		Verbosef("no policy was specified, no snapshots will be removed\n")
	}

	if !policy.Empty() {
		if !gopts.JSON {
			Verbosef("Applying Policy: %v\n", policy)
		}

		var jsonGroups []*ForgetGroup

		for k, snapshotGroup := range snapshotGroups {
			var key key
			if json.Unmarshal([]byte(k), &key) != nil {
				return err
			}

			var fg ForgetGroup
			// Info
			if !gopts.JSON {
				Verbosef("snapshots")
			}
			var infoStrings []string
			if GroupByTag {
				infoStrings = append(infoStrings, "tags ["+strings.Join(key.Tags, ", ")+"]")
				fg.Tags = key.Tags
			}
			if GroupByHost {
				infoStrings = append(infoStrings, "host ["+key.Hostname+"]")
				fg.Host = key.Hostname
			}
			if GroupByPath {
				infoStrings = append(infoStrings, "paths ["+strings.Join(key.Paths, ", ")+"]")
				fg.Paths = key.Paths
			}
			if infoStrings != nil && !gopts.JSON {
				Verbosef(" for (" + strings.Join(infoStrings, ", ") + ")")
			}
			if !gopts.JSON {
				Verbosef(":\n\n")
			}

			keep, remove, reasons := restic.ApplyPolicy(snapshotGroup, policy)

			if len(keep) != 0 && !gopts.Quiet && !gopts.JSON {
				Printf("keep %d snapshots:\n", len(keep))
				PrintSnapshots(globalOptions.stdout, keep, reasons, opts.Compact)
				Printf("\n")
			}
			addJSONSnapshots(&fg.Keep, keep)

			if len(remove) != 0 && !gopts.Quiet && !gopts.JSON {
				Printf("remove %d snapshots:\n", len(remove))
				PrintSnapshots(globalOptions.stdout, remove, nil, opts.Compact)
				Printf("\n")
			}
			addJSONSnapshots(&fg.Remove, remove)

			fg.Reasons = reasons

			jsonGroups = append(jsonGroups, &fg)

			removeSnapshots += len(remove)

			if !opts.DryRun {
				for _, sn := range remove {
					h := restic.Handle{Type: restic.SnapshotFile, Name: sn.ID().String()}
					err = repo.Backend().Remove(gopts.ctx, h)
					if err != nil {
						return err
					}
				}
			}
		}

		if gopts.JSON {
			err = printJSONForget(gopts.stdout, jsonGroups)
			if err != nil {
				return err
			}
		}
	}

	if removeSnapshots > 0 && opts.Prune {
		Verbosef("%d snapshots have been removed, running prune\n", removeSnapshots)
		if !opts.DryRun {
			return pruneRepository(gopts, repo)
		}
	}

	return nil
}

// ForgetGroup helps to print what is forgotten in JSON.
type ForgetGroup struct {
	Tags    []string            `json:"tags"`
	Host    string              `json:"host"`
	Paths   []string            `json:"paths"`
	Keep    []Snapshot          `json:"keep"`
	Remove  []Snapshot          `json:"remove"`
	Reasons []restic.KeepReason `json:"reasons"`
}

func addJSONSnapshots(js *[]Snapshot, list restic.Snapshots) {
	for _, sn := range list {
		k := Snapshot{
			Snapshot: sn,
			ID:       sn.ID(),
			ShortID:  sn.ID().Str(),
		}
		*js = append(*js, k)
	}
}

func printJSONForget(stdout io.Writer, forgets []*ForgetGroup) error {
	return json.NewEncoder(stdout).Encode(forgets)
}
<|endoftext|>


<commit_before>/*
	Go Language Raspberry Pi Interface
	(c) Copyright David Thorpe 2016-2017
	All Rights Reserved
	Documentation http://djthorpe.github.io/gopi/
	For Licensing and Usage information, please see LICENSE.md
*/

// Example command for discovery of RPC microservices using mDNS
package main

import (
	"context"
	"errors"
	"fmt"
	"os"
	"strings"
	"sync"
	"time"

	// Frameworks
	gopi "github.com/djthorpe/gopi"
	tablewriter "github.com/olekukonko/tablewriter"

	// Modules
	_ "github.com/djthorpe/gopi/sys/logger"
	_ "github.com/djthorpe/gopi/sys/rpc/mdns"
)

////////////////////////////////////////////////////////////////////////////////

var (
	lock  sync.Mutex
	gctx  context.Context
	table *tablewriter.Table
)

func InitContext() {
	lock.Lock()
}

func SetContext(ctx context.Context) {
	gctx = ctx
	defer lock.Unlock()
}

func GetContext() context.Context {
	lock.Lock()
	defer lock.Unlock()
	return gctx
}

////////////////////////////////////////////////////////////////////////////////

func PrintHeader() {
	table = tablewriter.NewWriter(os.Stdout)
	table.SetHeader([]string{"NAME", "TYPE", "ADDR:PORT", "TTL", "TEXT"})
	table.SetAutoMergeCells(true)
}

func PrintRecord(s *gopi.RPCServiceRecord) {
	// Gather up the addresses
	addr := ""
	for _, ip4 := range s.IP4 {
		addr += fmt.Sprintf("%v:%v\n", ip4.String(), s.Port)
	}
	for _, ip6 := range s.IP6 {
		addr += fmt.Sprintf("%v:%v\n", ip6.String(), s.Port)
	}
	table.Append([]string{
		s.Name,
		s.Type,
		strings.TrimSpace(addr),
		fmt.Sprint(s.TTL),
		strings.Join(s.Text, "\n"),
	})
	table.Render()
	table.ClearRows()

	/*	table.SetHeader("NAME", "TYPE", "ADDR:PORT", "TTL", "TEXT")
		fmt.Printf("%s\n", )
		fmt.Printf("%-4s %-20s %-20s %-10v %-10s\n", "", s.Type, "", s.Port, s.TTL)
		for _, txt := range s.Text {
			fmt.Printf("%-53s %s\n", "", txt)
		}*/
}

func EventLoop(app *gopi.AppInstance, done <-chan struct{}) error {
	var once sync.Once

	discovery := app.ModuleInstance("rpc/discovery").(gopi.RPCServiceDiscovery)

	// Subscribe to record discovery
	c := discovery.Subscribe()

FOR_LOOP:
	for {
		select {
		case evt := <-c:
			if rpc_evt, ok := evt.(gopi.RPCEvent); rpc_evt != nil && ok {
				once.Do(PrintHeader)
				PrintRecord(rpc_evt.ServiceRecord())
			}
		case <-done:
			break FOR_LOOP
		}
	}

	// Stop listening for events
	discovery.Unsubscribe(c)

	return nil
}

func BrowseLoop(app *gopi.AppInstance, done <-chan struct{}) error {

	if service, _ := app.AppFlags.GetString("service"); service == "" {
		return errors.New("Missing or invalid -service parameter")
	} else if service_qualified, err := gopi.RPCServiceType(service, gopi.RPC_FLAG_NONE); err != nil {
		return err
	} else {
		app.Logger.Info("Browsing for '%v'", service_qualified)
		if discovery, ok := app.ModuleInstance("rpc/discovery").(gopi.RPCServiceDiscovery); discovery == nil || ok == false {
			return errors.New("Missing or invalid discovery service")
		} else if err := discovery.Browse(GetContext(), service_qualified); err != nil {
			return err
		}
	}

	// Wait for done
	_ = <-done
	return nil
}

func MainLoop(app *gopi.AppInstance, done chan<- struct{}) error {
	// Set parameters
	timeout, _ := app.AppFlags.GetDuration("timeout")
	ctx, cancel := context.WithTimeout(context.Background(), timeout)
	SetContext(ctx)

	// Wait until CTRL+C is pressed
	app.WaitForSignalOrTimeout(timeout)

	// Perform cancel
	cancel()

	// Finish gracefully
	done <- gopi.DONE
	return nil
}

////////////////////////////////////////////////////////////////////////////////

func main() {
	// Create the configuration, load the lirc instance
	config := gopi.NewAppConfig("mdns")

	// Set flags
	config.AppFlags.FlagDuration("timeout", 1*time.Second, "Browse timeout")
	config.AppFlags.FlagString("service", "", "Service")

	// Init
	InitContext()

	// Run the command line tool
	os.Exit(gopi.CommandLineTool(config, MainLoop, BrowseLoop, EventLoop))
}
<commit_msg>Updated RPC discovery code<commit_after>/*
	Go Language Raspberry Pi Interface
	(c) Copyright David Thorpe 2016-2017
	All Rights Reserved
	Documentation http://djthorpe.github.io/gopi/
	For Licensing and Usage information, please see LICENSE.md
*/

// Example command for discovery of RPC microservices using mDNS
package main

import (
	"context"
	"errors"
	"fmt"
	"os"
	"strings"
	"sync"
	"time"

	// Frameworks
	gopi "github.com/djthorpe/gopi"
	tablewriter "github.com/olekukonko/tablewriter"

	// Modules
	_ "github.com/djthorpe/gopi/sys/logger"
	_ "github.com/djthorpe/gopi/sys/rpc/mdns"
)

////////////////////////////////////////////////////////////////////////////////

var (
	lock  sync.Mutex
	gctx  context.Context
	table *tablewriter.Table
)

func InitContext() {
	lock.Lock()
}

func SetContext(ctx context.Context) {
	gctx = ctx
	defer lock.Unlock()
}

func GetContext() context.Context {
	lock.Lock()
	defer lock.Unlock()
	return gctx
}

////////////////////////////////////////////////////////////////////////////////

func PrintHeader() {
	table = tablewriter.NewWriter(os.Stdout)
	table.SetHeader([]string{"NAME", "TYPE", "ADDR:PORT", "TTL", "TEXT"})
	table.SetAutoMergeCells(true)
}

func PrintRecord(s *gopi.RPCServiceRecord) {
	// Gather up the addresses
	addr := ""
	for _, ip4 := range s.IP4 {
		addr += fmt.Sprintf("%v:%v\n", ip4.String(), s.Port)
	}
	for _, ip6 := range s.IP6 {
		addr += fmt.Sprintf("%v:%v\n", ip6.String(), s.Port)
	}
	table.Append([]string{
		s.Name,
		s.Type,
		strings.TrimSpace(addr),
		fmt.Sprint(s.TTL),
		strings.Join(s.Text, "\n"),
	})
	table.Render()
	table.ClearRows()
}

func EventLoop(app *gopi.AppInstance, done <-chan struct{}) error {
	var once sync.Once

	discovery := app.ModuleInstance("rpc/discovery").(gopi.RPCServiceDiscovery)

	// Subscribe to record discovery
	c := discovery.Subscribe()

FOR_LOOP:
	for {
		select {
		case evt := <-c:
			if rpc_evt, ok := evt.(gopi.RPCEvent); rpc_evt != nil && ok {
				once.Do(PrintHeader)
				PrintRecord(rpc_evt.ServiceRecord())
			}
		case <-done:
			break FOR_LOOP
		}
	}

	// Stop listening for events
	discovery.Unsubscribe(c)

	return nil
}

func BrowseLoop(app *gopi.AppInstance, done <-chan struct{}) error {

	if service, _ := app.AppFlags.GetString("service"); service == "" {
		return errors.New("Missing or invalid -service parameter")
	} else if service_qualified, err := gopi.RPCServiceType(service, gopi.RPC_FLAG_NONE); err != nil {
		return err
	} else {
		app.Logger.Info("Browsing for '%v'", service_qualified)
		if discovery, ok := app.ModuleInstance("rpc/discovery").(gopi.RPCServiceDiscovery); discovery == nil || ok == false {
			return errors.New("Missing or invalid discovery service")
		} else if err := discovery.Browse(GetContext(), service_qualified); err != nil {
			return err
		}
	}

	// Wait for done
	_ = <-done
	return nil
}

func MainLoop(app *gopi.AppInstance, done chan<- struct{}) error {
	// Set parameters
	timeout, _ := app.AppFlags.GetDuration("timeout")
	ctx, cancel := context.WithTimeout(context.Background(), timeout)
	SetContext(ctx)

	// Wait until CTRL+C is pressed
	app.WaitForSignalOrTimeout(timeout)

	// Perform cancel
	cancel()

	// Finish gracefully
	done <- gopi.DONE
	return nil
}

////////////////////////////////////////////////////////////////////////////////

func main() {
	// Create the configuration, load the lirc instance
	config := gopi.NewAppConfig("mdns")

	// Set flags
	config.AppFlags.FlagDuration("timeout", 1*time.Second, "Browse timeout")
	config.AppFlags.FlagString("service", "", "Service")

	// Init
	InitContext()

	// Run the command line tool
	os.Exit(gopi.CommandLineTool(config, MainLoop, BrowseLoop, EventLoop))
}
<|endoftext|>


<commit_before>package main

import (
	"bufio"
	"encoding/hex"
	"errors"
	"fmt"
	"io"
	"os"
	"strings"

	"github.com/codegangsta/cli"
	. "github.com/tendermint/go-common"
	"github.com/tendermint/tmsp/client"
	"github.com/tendermint/tmsp/types"
)

// client is a global variable so it can be reused by the console
var client tmspcli.Client

func main() {
	app := cli.NewApp()
	app.Name = "tmsp-cli"
	app.Usage = "tmsp-cli [command] [args...]"
	app.Version = "0.2"
	app.Flags = []cli.Flag{
		cli.StringFlag{
			Name:  "address",
			Value: "tcp://127.0.0.1:46658",
			Usage: "address of application socket",
		},
		cli.StringFlag{
			Name:  "tmsp",
			Value: "socket",
			Usage: "socket or grpc",
		},
		cli.BoolFlag{
			Name:  "verbose",
			Usage: "print the command and results as if it were a console session",
		},
	}
	app.Commands = []cli.Command{
		{
			Name:  "batch",
			Usage: "Run a batch of tmsp commands against an application",
			Action: func(c *cli.Context) error {
				return cmdBatch(app, c)
			},
		},
		{
			Name:  "console",
			Usage: "Start an interactive tmsp console for multiple commands",
			Action: func(c *cli.Context) error {
				return cmdConsole(app, c)
			},
		},
		{
			Name:  "echo",
			Usage: "Have the application echo a message",
			Action: func(c *cli.Context) error {
				return cmdEcho(c)
			},
		},
		{
			Name:  "info",
			Usage: "Get some info about the application",
			Action: func(c *cli.Context) error {
				return cmdInfo(c)
			},
		},
		{
			Name:  "set_option",
			Usage: "Set an option on the application",
			Action: func(c *cli.Context) error {
				return cmdSetOption(c)
			},
		},
		{
			Name:  "append_tx",
			Usage: "Append a new tx to application",
			Action: func(c *cli.Context) error {
				return cmdAppendTx(c)
			},
		},
		{
			Name:  "check_tx",
			Usage: "Validate a tx",
			Action: func(c *cli.Context) error {
				return cmdCheckTx(c)
			},
		},
		{
			Name:  "commit",
			Usage: "Commit the application state and return the Merkle root hash",
			Action: func(c *cli.Context) error {
				return cmdCommit(c)
			},
		},
		{
			Name:  "query",
			Usage: "Query application state",
			Action: func(c *cli.Context) error {
				return cmdQuery(c)
			},
		},
	}
	app.Before = before
	err := app.Run(os.Args)
	if err != nil {
		Exit(err.Error())
	}

}

func before(c *cli.Context) error {
	if client == nil {
		var err error
		client, err = tmspcli.NewClient(c.GlobalString("address"), c.GlobalString("tmsp"), false)
		if err != nil {
			Exit(err.Error())
		}
	}
	return nil
}

//--------------------------------------------------------------------------------

func cmdBatch(app *cli.App, c *cli.Context) error {
	bufReader := bufio.NewReader(os.Stdin)
	for {
		line, more, err := bufReader.ReadLine()
		if more {
			return errors.New("Input line is too long")
		} else if err == io.EOF {
			break
		} else if len(line) == 0 {
			continue
		} else if err != nil {
			return err
		}
		args := []string{"tmsp-cli"}
		if c.GlobalBool("verbose") {
			args = append(args, "--verbose")
		}
		args = append(args, strings.Split(string(line), " ")...)
		app.Run(args)
	}
	return nil
}

func cmdConsole(app *cli.App, c *cli.Context) error {
	for {
		fmt.Printf("\n> ")
		bufReader := bufio.NewReader(os.Stdin)
		line, more, err := bufReader.ReadLine()
		if more {
			return errors.New("Input is too long")
		} else if err != nil {
			return err
		}

		args := []string{"tmsp-cli"}
		args = append(args, strings.Split(string(line), " ")...)
		if err := app.Run(args); err != nil {
			return err
		}
	}
	return nil
}

// Have the application echo a message
func cmdEcho(c *cli.Context) error {
	args := c.Args()
	if len(args) != 1 {
		return errors.New("Command echo takes 1 argument")
	}
	res := client.EchoSync(args[0])
	printResponse(c, res, string(res.Data), false)
	return nil
}

// Get some info from the application
func cmdInfo(c *cli.Context) error {
	res := client.InfoSync()
	printResponse(c, res, string(res.Data), false)
	return nil
}

// Set an option on the application
func cmdSetOption(c *cli.Context) error {
	args := c.Args()
	if len(args) != 2 {
		return errors.New("Command set_option takes 2 arguments (key, value)")
	}
	res := client.SetOptionSync(args[0], args[1])
	printResponse(c, res, Fmt("%s=%s", args[0], args[1]), false)
	return nil
}

// Append a new tx to application
func cmdAppendTx(c *cli.Context) error {
	args := c.Args()
	if len(args) != 1 {
		return errors.New("Command append_tx takes 1 argument")
	}
	txBytes := stringOrHexToBytes(c.Args()[0])
	res := client.AppendTxSync(txBytes)
	printResponse(c, res, string(res.Data), true)
	return nil
}

// Validate a tx
func cmdCheckTx(c *cli.Context) error {
	args := c.Args()
	if len(args) != 1 {
		return errors.New("Command check_tx takes 1 argument")
	}
	txBytes := stringOrHexToBytes(c.Args()[0])
	res := client.CheckTxSync(txBytes)
	printResponse(c, res, string(res.Data), true)
	return nil
}

// Get application Merkle root hash
func cmdCommit(c *cli.Context) error {
	res := client.CommitSync()
	printResponse(c, res, Fmt("%X", res.Data), false)
	return nil
}

// Query application state
func cmdQuery(c *cli.Context) error {
	args := c.Args()
	if len(args) != 1 {
		return errors.New("Command query takes 1 argument")
	}
	queryBytes := stringOrHexToBytes(c.Args()[0])
	res := client.QuerySync(queryBytes)
	printResponse(c, res, string(res.Data), true)
	return nil
}

//--------------------------------------------------------------------------------

func printResponse(c *cli.Context, res types.Result, s string, printCode bool) {
	if c.GlobalBool("verbose") {
		fmt.Println(">", c.Command.Name, strings.Join(c.Args(), " "))
	}

	if printCode {
		fmt.Printf("-> code: %s\n", res.Code.String())
	}
	/*if res.Error != "" {
		fmt.Printf("-> error: %s\n", res.Error)
	}*/
	if s != "" {
		fmt.Printf("-> data: {%s}\n", s)
	}
	if res.Log != "" {
		fmt.Printf("-> log: %s\n", res.Log)
	}

	if c.GlobalBool("verbose") {
		fmt.Println("")
	}

}

// NOTE: s is interpreted as a string unless prefixed with 0x
func stringOrHexToBytes(s string) []byte {
	if len(s) > 2 && s[:2] == "0x" {
		b, err := hex.DecodeString(s[2:])
		if err != nil {
			fmt.Println("Error decoding hex argument:", err.Error())
		}
		return b
	}
	return []byte(s)
}
<commit_msg>Make console more robust to typos<commit_after>package main

import (
	"bufio"
	"encoding/hex"
	"errors"
	"fmt"
	"io"
	"os"
	"strings"

	"github.com/codegangsta/cli"
	. "github.com/tendermint/go-common"
	"github.com/tendermint/tmsp/client"
	"github.com/tendermint/tmsp/types"
)

// client is a global variable so it can be reused by the console
var client tmspcli.Client

func main() {
	app := cli.NewApp()
	app.Name = "tmsp-cli"
	app.Usage = "tmsp-cli [command] [args...]"
	app.Version = "0.2"
	app.Flags = []cli.Flag{
		cli.StringFlag{
			Name:  "address",
			Value: "tcp://127.0.0.1:46658",
			Usage: "address of application socket",
		},
		cli.StringFlag{
			Name:  "tmsp",
			Value: "socket",
			Usage: "socket or grpc",
		},
		cli.BoolFlag{
			Name:  "verbose",
			Usage: "print the command and results as if it were a console session",
		},
	}
	app.Commands = []cli.Command{
		{
			Name:  "batch",
			Usage: "Run a batch of tmsp commands against an application",
			Action: func(c *cli.Context) error {
				return cmdBatch(app, c)
			},
		},
		{
			Name:  "console",
			Usage: "Start an interactive tmsp console for multiple commands",
			Action: func(c *cli.Context) error {
				return cmdConsole(app, c)
			},
		},
		{
			Name:  "echo",
			Usage: "Have the application echo a message",
			Action: func(c *cli.Context) error {
				return cmdEcho(c)
			},
		},
		{
			Name:  "info",
			Usage: "Get some info about the application",
			Action: func(c *cli.Context) error {
				return cmdInfo(c)
			},
		},
		{
			Name:  "set_option",
			Usage: "Set an option on the application",
			Action: func(c *cli.Context) error {
				return cmdSetOption(c)
			},
		},
		{
			Name:  "append_tx",
			Usage: "Append a new tx to application",
			Action: func(c *cli.Context) error {
				return cmdAppendTx(c)
			},
		},
		{
			Name:  "check_tx",
			Usage: "Validate a tx",
			Action: func(c *cli.Context) error {
				return cmdCheckTx(c)
			},
		},
		{
			Name:  "commit",
			Usage: "Commit the application state and return the Merkle root hash",
			Action: func(c *cli.Context) error {
				return cmdCommit(c)
			},
		},
		{
			Name:  "query",
			Usage: "Query application state",
			Action: func(c *cli.Context) error {
				return cmdQuery(c)
			},
		},
	}
	app.Before = before
	err := app.Run(os.Args)
	if err != nil {
		Exit(err.Error())
	}

}

func before(c *cli.Context) error {
	if client == nil {
		var err error
		client, err = tmspcli.NewClient(c.GlobalString("address"), c.GlobalString("tmsp"), false)
		if err != nil {
			Exit(err.Error())
		}
	}
	return nil
}

// badCmd is called when we invoke with an invalid first argument (just for console for now)
func badCmd(c *cli.Context, cmd string) {
	fmt.Println("Unknown command:", cmd)
	fmt.Println("Please try one of the following:")
	fmt.Println("")
	cli.DefaultAppComplete(c)
}

//--------------------------------------------------------------------------------

func cmdBatch(app *cli.App, c *cli.Context) error {
	bufReader := bufio.NewReader(os.Stdin)
	for {
		line, more, err := bufReader.ReadLine()
		if more {
			return errors.New("Input line is too long")
		} else if err == io.EOF {
			break
		} else if len(line) == 0 {
			continue
		} else if err != nil {
			return err
		}
		args := []string{"tmsp-cli"}
		if c.GlobalBool("verbose") {
			args = append(args, "--verbose")
		}
		args = append(args, strings.Split(string(line), " ")...)
		app.Run(args)
	}
	return nil
}

func cmdConsole(app *cli.App, c *cli.Context) error {
	// don't hard exit on mistyped commands (eg. check vs check_tx)
	app.CommandNotFound = badCmd
	for {
		fmt.Printf("\n> ")
		bufReader := bufio.NewReader(os.Stdin)
		line, more, err := bufReader.ReadLine()
		if more {
			return errors.New("Input is too long")
		} else if err != nil {
			return err
		}

		args := []string{"tmsp-cli"}
		args = append(args, strings.Split(string(line), " ")...)
		if err := app.Run(args); err != nil {
			// if the command doesn't succeed, inform the user without exiting
			fmt.Println("Error:", err.Error())
		}
	}
}

// Have the application echo a message
func cmdEcho(c *cli.Context) error {
	args := c.Args()
	if len(args) != 1 {
		return errors.New("Command echo takes 1 argument")
	}
	res := client.EchoSync(args[0])
	printResponse(c, res, string(res.Data), false)
	return nil
}

// Get some info from the application
func cmdInfo(c *cli.Context) error {
	res := client.InfoSync()
	printResponse(c, res, string(res.Data), false)
	return nil
}

// Set an option on the application
func cmdSetOption(c *cli.Context) error {
	args := c.Args()
	if len(args) != 2 {
		return errors.New("Command set_option takes 2 arguments (key, value)")
	}
	res := client.SetOptionSync(args[0], args[1])
	printResponse(c, res, Fmt("%s=%s", args[0], args[1]), false)
	return nil
}

// Append a new tx to application
func cmdAppendTx(c *cli.Context) error {
	args := c.Args()
	if len(args) != 1 {
		return errors.New("Command append_tx takes 1 argument")
	}
	txBytes := stringOrHexToBytes(c.Args()[0])
	res := client.AppendTxSync(txBytes)
	printResponse(c, res, string(res.Data), true)
	return nil
}

// Validate a tx
func cmdCheckTx(c *cli.Context) error {
	args := c.Args()
	if len(args) != 1 {
		return errors.New("Command check_tx takes 1 argument")
	}
	txBytes := stringOrHexToBytes(c.Args()[0])
	res := client.CheckTxSync(txBytes)
	printResponse(c, res, string(res.Data), true)
	return nil
}

// Get application Merkle root hash
func cmdCommit(c *cli.Context) error {
	res := client.CommitSync()
	printResponse(c, res, Fmt("%X", res.Data), false)
	return nil
}

// Query application state
func cmdQuery(c *cli.Context) error {
	args := c.Args()
	if len(args) != 1 {
		return errors.New("Command query takes 1 argument")
	}
	queryBytes := stringOrHexToBytes(c.Args()[0])
	res := client.QuerySync(queryBytes)
	printResponse(c, res, string(res.Data), true)
	return nil
}

//--------------------------------------------------------------------------------

func printResponse(c *cli.Context, res types.Result, s string, printCode bool) {
	if c.GlobalBool("verbose") {
		fmt.Println(">", c.Command.Name, strings.Join(c.Args(), " "))
	}

	if printCode {
		fmt.Printf("-> code: %s\n", res.Code.String())
	}
	/*if res.Error != "" {
		fmt.Printf("-> error: %s\n", res.Error)
	}*/
	if s != "" {
		fmt.Printf("-> data: {%s}\n", s)
	}
	if res.Log != "" {
		fmt.Printf("-> log: %s\n", res.Log)
	}

	if c.GlobalBool("verbose") {
		fmt.Println("")
	}

}

// NOTE: s is interpreted as a string unless prefixed with 0x
func stringOrHexToBytes(s string) []byte {
	if len(s) > 2 && s[:2] == "0x" {
		b, err := hex.DecodeString(s[2:])
		if err != nil {
			fmt.Println("Error decoding hex argument:", err.Error())
		}
		return b
	}
	return []byte(s)
}
<|endoftext|>


<commit_before>package http

import (
	"errors"
	"math/rand"
	"net"
	"net/http"
	"net/url"
	"strconv"
	"testing"
	"time"

	"github.com/pierrre/imageserver"
	imageserver_provider "github.com/pierrre/imageserver/provider"
	"github.com/pierrre/imageserver/testdata"
)

var (
	testSourceFileName = testdata.SmallFileName
)

func init() {
	rand.Seed(time.Now().UnixNano())
}

func TestInterface(t *testing.T) {
	var _ imageserver_provider.Provider = &Provider{}
}

func TestGet(t *testing.T) {
	listener := createTestHTTPServer(t)
	defer listener.Close()

	source := createTestURL(listener)
	parameters := make(imageserver.Parameters)

	provider := &Provider{}

	image, err := provider.Get(source, parameters)
	if err != nil {
		t.Fatal(err)
	}
	if image == nil {
		t.Fatal("no image")
	}
	if len(image.Data) == 0 {
		t.Fatal("no data")
	}
	if image.Format == "" {
		t.Fatal("no format")
	}
}

func TestGetErrorNotFound(t *testing.T) {
	listener := createTestHTTPServer(t)
	defer listener.Close()

	source := createTestURL(listener)
	source.Path += "foobar"
	parameters := make(imageserver.Parameters)

	provider := &Provider{}

	_, err := provider.Get(source, parameters)
	if err == nil {
		t.Fatal("no error")
	}
}

func TestGetErrorInvalidUrl(t *testing.T) {
	listener := createTestHTTPServer(t)
	defer listener.Close()

	source := "foobar"
	parameters := make(imageserver.Parameters)

	provider := &Provider{}

	_, err := provider.Get(source, parameters)
	if err == nil {
		t.Fatal("no error")
	}
}

func TestGetErrorInvalidUrlScheme(t *testing.T) {
	listener := createTestHTTPServer(t)
	defer listener.Close()

	source := "custom://foobar"
	parameters := make(imageserver.Parameters)

	provider := &Provider{}

	_, err := provider.Get(source, parameters)
	if err == nil {
		t.Fatal("no error")
	}
}

func TestGetErrorRequest(t *testing.T) {
	listener := createTestHTTPServer(t)
	defer listener.Close()

	source := "http://invalidhost" + strconv.Itoa(rand.Int())
	parameters := make(imageserver.Parameters)

	provider := &Provider{}

	_, err := provider.Get(source, parameters)
	if err == nil {
		t.Fatal("no error")
	}
}

type errorReadCloser struct{}

func (erc *errorReadCloser) Read(p []byte) (n int, err error) {
	return 0, errors.New("error")
}

func (erc *errorReadCloser) Close() error {
	return errors.New("error")
}

func TestParseResponseErrorData(t *testing.T) {
	provider := &Provider{}

	response := &http.Response{
		StatusCode: http.StatusOK,
		Body:       &errorReadCloser{},
	}

	_, err := provider.parseResponse(response)
	t.Log(err)
	if err == nil {
		t.Fatal("no error")
	}
}

func createTestHTTPServer(t *testing.T) *net.TCPListener {
	addr, err := net.ResolveTCPAddr("tcp", "")
	if err != nil {
		t.Fatal(err)
	}

	listener, err := net.ListenTCP("tcp", addr)
	if err != nil {
		t.Fatal(err)
	}

	server := &http.Server{
		Handler: http.FileServer(http.Dir(testdata.Dir)),
	}
	go server.Serve(listener)

	return listener
}

func createTestURL(listener *net.TCPListener) *url.URL {
	return &url.URL{
		Scheme: "http",
		Host:   listener.Addr().String(),
		Path:   testSourceFileName,
	}
}
<commit_msg>remove log in test<commit_after>package http

import (
	"errors"
	"math/rand"
	"net"
	"net/http"
	"net/url"
	"strconv"
	"testing"
	"time"

	"github.com/pierrre/imageserver"
	imageserver_provider "github.com/pierrre/imageserver/provider"
	"github.com/pierrre/imageserver/testdata"
)

var (
	testSourceFileName = testdata.SmallFileName
)

func init() {
	rand.Seed(time.Now().UnixNano())
}

func TestInterface(t *testing.T) {
	var _ imageserver_provider.Provider = &Provider{}
}

func TestGet(t *testing.T) {
	listener := createTestHTTPServer(t)
	defer listener.Close()

	source := createTestURL(listener)
	parameters := make(imageserver.Parameters)

	provider := &Provider{}

	image, err := provider.Get(source, parameters)
	if err != nil {
		t.Fatal(err)
	}
	if image == nil {
		t.Fatal("no image")
	}
	if len(image.Data) == 0 {
		t.Fatal("no data")
	}
	if image.Format == "" {
		t.Fatal("no format")
	}
}

func TestGetErrorNotFound(t *testing.T) {
	listener := createTestHTTPServer(t)
	defer listener.Close()

	source := createTestURL(listener)
	source.Path += "foobar"
	parameters := make(imageserver.Parameters)

	provider := &Provider{}

	_, err := provider.Get(source, parameters)
	if err == nil {
		t.Fatal("no error")
	}
}

func TestGetErrorInvalidUrl(t *testing.T) {
	listener := createTestHTTPServer(t)
	defer listener.Close()

	source := "foobar"
	parameters := make(imageserver.Parameters)

	provider := &Provider{}

	_, err := provider.Get(source, parameters)
	if err == nil {
		t.Fatal("no error")
	}
}

func TestGetErrorInvalidUrlScheme(t *testing.T) {
	listener := createTestHTTPServer(t)
	defer listener.Close()

	source := "custom://foobar"
	parameters := make(imageserver.Parameters)

	provider := &Provider{}

	_, err := provider.Get(source, parameters)
	if err == nil {
		t.Fatal("no error")
	}
}

func TestGetErrorRequest(t *testing.T) {
	listener := createTestHTTPServer(t)
	defer listener.Close()

	source := "http://invalidhost" + strconv.Itoa(rand.Int())
	parameters := make(imageserver.Parameters)

	provider := &Provider{}

	_, err := provider.Get(source, parameters)
	if err == nil {
		t.Fatal("no error")
	}
}

type errorReadCloser struct{}

func (erc *errorReadCloser) Read(p []byte) (n int, err error) {
	return 0, errors.New("error")
}

func (erc *errorReadCloser) Close() error {
	return errors.New("error")
}

func TestParseResponseErrorData(t *testing.T) {
	provider := &Provider{}

	response := &http.Response{
		StatusCode: http.StatusOK,
		Body:       &errorReadCloser{},
	}

	_, err := provider.parseResponse(response)
	if err == nil {
		t.Fatal("no error")
	}
}

func createTestHTTPServer(t *testing.T) *net.TCPListener {
	addr, err := net.ResolveTCPAddr("tcp", "")
	if err != nil {
		t.Fatal(err)
	}

	listener, err := net.ListenTCP("tcp", addr)
	if err != nil {
		t.Fatal(err)
	}

	server := &http.Server{
		Handler: http.FileServer(http.Dir(testdata.Dir)),
	}
	go server.Serve(listener)

	return listener
}

func createTestURL(listener *net.TCPListener) *url.URL {
	return &url.URL{
		Scheme: "http",
		Host:   listener.Addr().String(),
		Path:   testSourceFileName,
	}
}
<|endoftext|>


<commit_before>// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// Contributor:
// - Aaron Meihm ameihm@mozilla.com

package main

import (
	"encoding/json"
	"fmt"
	slib "github.com/mozilla/service-map/servicelib"
	"net/http"
	"strconv"
)

// Return an AssetGroup given an asset group ID
func getAssetGroup(op opContext, agid int) (ret slib.AssetGroup, err error) {
	err = op.QueryRow(`SELECT assetgroupid, name
		FROM assetgroup WHERE assetgroupid = $1`, agid).Scan(&ret.ID, &ret.Name)
	return
}

// Get all asset groups
func getAssetGroups(op opContext) (ret []slib.AssetGroup, err error) {
	rows, err := op.Query(`SELECT assetgroupid, name
		FROM assetgroup`)
	if err != nil {
		return
	}
	for rows.Next() {
		var ngrp slib.AssetGroup
		err = rows.Scan(&ngrp.ID, &ngrp.Name)
		if err != nil {
			rows.Close()
			return ret, err
		}
		ret = append(ret, ngrp)
	}
	err = rows.Err()
	if err != nil {
		return
	}
	return
}

// API entry point to retrieve a given system group
func serviceGetAssetGroup(rw http.ResponseWriter, req *http.Request) {
	op := opContext{}
	op.newContext(dbconn, false, req.RemoteAddr)

	req.ParseForm()
	agidstr := req.FormValue("id")
	if agidstr == "" {
		http.Error(rw, "invalid asset group id", 400)
		return
	}
	agid, err := strconv.Atoi(agidstr)
	if err != nil {
		op.logf(err.Error())
		http.Error(rw, "invalid asset group id", 400)
		return
	}

	ag, err := getAssetGroup(op, agid)
	if err != nil {
		op.logf(err.Error())
		http.Error(rw, "error retrieving asset group", 500)
		return
	}

	buf, err := json.Marshal(&ag)
	if err != nil {
		op.logf(err.Error())
		http.Error(rw, "error retrieving asset group", 500)
		return
	}
	fmt.Fprintf(rw, string(buf))
}

// API entry point to retrieve all asset groups
func serviceAssetGroups(rw http.ResponseWriter, req *http.Request) {
	op := opContext{}
	op.newContext(dbconn, false, req.RemoteAddr)

	var err error
	agr := slib.AssetGroupsResponse{}
	agr.Groups, err = getAssetGroups(op)
	if err != nil {
		op.logf(err.Error())
		http.Error(rw, "error retrieving asset groups", 500)
		return
	}
	buf, err := json.Marshal(&agr)
	if err != nil {
		op.logf(err.Error())
		http.Error(rw, "error retrieving asset groups", 500)
		return
	}
	fmt.Fprint(rw, string(buf))
}
<commit_msg>update comment to reference asset group<commit_after>// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// Contributor:
// - Aaron Meihm ameihm@mozilla.com

package main

import (
	"encoding/json"
	"fmt"
	slib "github.com/mozilla/service-map/servicelib"
	"net/http"
	"strconv"
)

// Return an AssetGroup given an asset group ID
func getAssetGroup(op opContext, agid int) (ret slib.AssetGroup, err error) {
	err = op.QueryRow(`SELECT assetgroupid, name
		FROM assetgroup WHERE assetgroupid = $1`, agid).Scan(&ret.ID, &ret.Name)
	return
}

// Get all asset groups
func getAssetGroups(op opContext) (ret []slib.AssetGroup, err error) {
	rows, err := op.Query(`SELECT assetgroupid, name
		FROM assetgroup`)
	if err != nil {
		return
	}
	for rows.Next() {
		var ngrp slib.AssetGroup
		err = rows.Scan(&ngrp.ID, &ngrp.Name)
		if err != nil {
			rows.Close()
			return ret, err
		}
		ret = append(ret, ngrp)
	}
	err = rows.Err()
	if err != nil {
		return
	}
	return
}

// API entry point to retrieve a given asset group
func serviceGetAssetGroup(rw http.ResponseWriter, req *http.Request) {
	op := opContext{}
	op.newContext(dbconn, false, req.RemoteAddr)

	req.ParseForm()
	agidstr := req.FormValue("id")
	if agidstr == "" {
		http.Error(rw, "invalid asset group id", 400)
		return
	}
	agid, err := strconv.Atoi(agidstr)
	if err != nil {
		op.logf(err.Error())
		http.Error(rw, "invalid asset group id", 400)
		return
	}

	ag, err := getAssetGroup(op, agid)
	if err != nil {
		op.logf(err.Error())
		http.Error(rw, "error retrieving asset group", 500)
		return
	}

	buf, err := json.Marshal(&ag)
	if err != nil {
		op.logf(err.Error())
		http.Error(rw, "error retrieving asset group", 500)
		return
	}
	fmt.Fprintf(rw, string(buf))
}

// API entry point to retrieve all asset groups
func serviceAssetGroups(rw http.ResponseWriter, req *http.Request) {
	op := opContext{}
	op.newContext(dbconn, false, req.RemoteAddr)

	var err error
	agr := slib.AssetGroupsResponse{}
	agr.Groups, err = getAssetGroups(op)
	if err != nil {
		op.logf(err.Error())
		http.Error(rw, "error retrieving asset groups", 500)
		return
	}
	buf, err := json.Marshal(&agr)
	if err != nil {
		op.logf(err.Error())
		http.Error(rw, "error retrieving asset groups", 500)
		return
	}
	fmt.Fprint(rw, string(buf))
}
<|endoftext|>


<commit_before>package channels

type BatteryDevice interface {
}

type BatteryChannel struct {
	baseChannel
	device BatteryDevice
}

func NewBatteryChannel(device BatteryDevice) *BatteryChannel {
	return &BatteryChannel{baseChannel{
		protocol: "battery",
	}, device}
}

func (c *BatteryChannel) SendState(state float64) error {
	return c.SendEvent("state", state)
}
<commit_msg>BatteryChannel - warning event<commit_after>package channels

type BatteryDevice interface {
}

type BatteryChannel struct {
	baseChannel
	device BatteryDevice
}

func NewBatteryChannel(device BatteryDevice) *BatteryChannel {
	return &BatteryChannel{baseChannel{
		protocol: "battery",
	}, device}
}

func (c *BatteryChannel) SendState(state float64) error {
	return c.SendEvent("state", state)
}

func (c *BatteryChannel) SendWarning() error {
	return c.SendEvent("warning")
}
<|endoftext|>


<commit_before>package pages

import (
	"strings"

	"github.com/aerogo/aero"
	"github.com/animenotifier/notify.moe/pages/index/amvroutes"
	"github.com/animenotifier/notify.moe/pages/index/animeroutes"
	"github.com/animenotifier/notify.moe/pages/index/apiroutes"
	"github.com/animenotifier/notify.moe/pages/index/characterroutes"
	"github.com/animenotifier/notify.moe/pages/index/companyroutes"
	"github.com/animenotifier/notify.moe/pages/index/coreroutes"
	"github.com/animenotifier/notify.moe/pages/index/exploreroutes"
	"github.com/animenotifier/notify.moe/pages/index/forumroutes"
	"github.com/animenotifier/notify.moe/pages/index/grouproutes"
	"github.com/animenotifier/notify.moe/pages/index/importroutes"
	"github.com/animenotifier/notify.moe/pages/index/quoteroutes"
	"github.com/animenotifier/notify.moe/pages/index/searchroutes"
	"github.com/animenotifier/notify.moe/pages/index/settingsroutes"
	"github.com/animenotifier/notify.moe/pages/index/shoproutes"
	"github.com/animenotifier/notify.moe/pages/index/soundtrackroutes"
	"github.com/animenotifier/notify.moe/pages/index/staffroutes"
	"github.com/animenotifier/notify.moe/pages/index/userlistroutes"
	"github.com/animenotifier/notify.moe/pages/index/userroutes"
)

// Configure registers the page routes in the application.
func Configure(app *aero.Application) {
	// Register the routes
	coreroutes.Register(app)
	userroutes.Register(app)
	characterroutes.Register(app)
	exploreroutes.Register(app)
	amvroutes.Register(app)
	forumroutes.Register(app)
	animeroutes.Register(app)
	userlistroutes.Register(app)
	quoteroutes.Register(app)
	companyroutes.Register(app)
	soundtrackroutes.Register(app)
	grouproutes.Register(app)
	searchroutes.Register(app)
	importroutes.Register(app)
	shoproutes.Register(app)
	settingsroutes.Register(app)
	staffroutes.Register(app)
	apiroutes.Register(app)

	// Mixed
	// app.Get("/database", database.Get)
	// app.Get("/api/select/:data-type/where/:field/is/:field-value", database.Select)
}

// Rewrite will rewrite the path before routing happens.
func Rewrite(ctx aero.RewriteContext) {
	requestURI := ctx.Path()

	// User profiles
	if strings.HasPrefix(requestURI, "/+") {
		newURI := "/user/"
		userName := requestURI[2:]
		ctx.SetPath(newURI + userName)
		return
	}

	if strings.HasPrefix(requestURI, "/_/+") {
		newURI := "/_/user/"
		userName := requestURI[4:]
		ctx.SetPath(newURI + userName)
		return
	}

	// Analytics
	if requestURI == "/dark-flame-master" {
		ctx.SetPath("/api/new/analytics")
		return
	}
}
<commit_msg>Minor change<commit_after>package pages

import (
	"strings"

	"github.com/aerogo/aero"
	"github.com/animenotifier/notify.moe/pages/index/amvroutes"
	"github.com/animenotifier/notify.moe/pages/index/animeroutes"
	"github.com/animenotifier/notify.moe/pages/index/apiroutes"
	"github.com/animenotifier/notify.moe/pages/index/characterroutes"
	"github.com/animenotifier/notify.moe/pages/index/companyroutes"
	"github.com/animenotifier/notify.moe/pages/index/coreroutes"
	"github.com/animenotifier/notify.moe/pages/index/exploreroutes"
	"github.com/animenotifier/notify.moe/pages/index/forumroutes"
	"github.com/animenotifier/notify.moe/pages/index/grouproutes"
	"github.com/animenotifier/notify.moe/pages/index/importroutes"
	"github.com/animenotifier/notify.moe/pages/index/quoteroutes"
	"github.com/animenotifier/notify.moe/pages/index/searchroutes"
	"github.com/animenotifier/notify.moe/pages/index/settingsroutes"
	"github.com/animenotifier/notify.moe/pages/index/shoproutes"
	"github.com/animenotifier/notify.moe/pages/index/soundtrackroutes"
	"github.com/animenotifier/notify.moe/pages/index/staffroutes"
	"github.com/animenotifier/notify.moe/pages/index/userlistroutes"
	"github.com/animenotifier/notify.moe/pages/index/userroutes"
)

// Configure registers the page routes in the application.
func Configure(app *aero.Application) {
	// Register the routes
	coreroutes.Register(app)
	userroutes.Register(app)
	characterroutes.Register(app)
	exploreroutes.Register(app)
	amvroutes.Register(app)
	forumroutes.Register(app)
	animeroutes.Register(app)
	userlistroutes.Register(app)
	quoteroutes.Register(app)
	companyroutes.Register(app)
	soundtrackroutes.Register(app)
	grouproutes.Register(app)
	searchroutes.Register(app)
	importroutes.Register(app)
	shoproutes.Register(app)
	settingsroutes.Register(app)
	staffroutes.Register(app)
	apiroutes.Register(app)
}

// Rewrite will rewrite the path before routing happens.
func Rewrite(ctx aero.RewriteContext) {
	requestURI := ctx.Path()

	// User profiles
	if strings.HasPrefix(requestURI, "/+") {
		newURI := "/user/"
		userName := requestURI[2:]
		ctx.SetPath(newURI + userName)
		return
	}

	if strings.HasPrefix(requestURI, "/_/+") {
		newURI := "/_/user/"
		userName := requestURI[4:]
		ctx.SetPath(newURI + userName)
		return
	}

	// Analytics
	if requestURI == "/dark-flame-master" {
		ctx.SetPath("/api/new/analytics")
		return
	}
}
<|endoftext|>


<commit_before>package contractmanager

import (
	"encoding/json"
	"path/filepath"
	"sync"
	"time"

	"github.com/NebulousLabs/Sia/build"
)

// syncResources will call Sync on all resources that the WAL has open. The
// storage folder files will be left open, as they are not updated atomically.
// The settings file and WAL tmp files will be synced and closed, to perform an
// atomic update to the files.
func (wal *writeAheadLog) syncResources() {
	// Syncing occurs over multiple files and disks, and is done in parallel to
	// minimize the amount of time that a lock is held over the contract
	// manager.
	var wg sync.WaitGroup

	// Sync the settings file.
	wg.Add(1)
	go func() {
		defer wg.Done()

		tmpFilename := filepath.Join(wal.cm.persistDir, settingsFileTmp)
		filename := filepath.Join(wal.cm.persistDir, settingsFile)
		err := wal.fileSettingsTmp.Sync()
		if err != nil {
			wal.cm.log.Severe("ERROR: unable to sync the contract manager settings:", err)
		}
		err = wal.fileSettingsTmp.Close()
		if err != nil {
			wal.cm.log.Println("unable to close the temporary contract manager settings file:", err)
		}

		// For testing, provide a place to interrupt the saving of the sync
		// file. This makes it easy to simulate certain types of unclean
		// shutdown.
		if wal.cm.dependencies.disrupt("settingsSyncRename") {
			// The current settings file that is being re-written will not be
			// saved.
			return
		}

		err = wal.cm.dependencies.renameFile(tmpFilename, filename)
		if err != nil {
			wal.cm.log.Severe("ERROR: unable to atomically copy the contract manager settings:", err)
		}
	}()

	// Sync all of the storage folders.
	for _, sf := range wal.cm.storageFolders {
		wg.Add(2)
		go func(sf *storageFolder) {
			defer wg.Done()
			err := sf.metadataFile.Sync()
			if err != nil {
				wal.cm.log.Severe("ERROR: unable to sync a storage folder:", err)
			}
		}(sf)
		go func(sf *storageFolder) {
			defer wg.Done()
			err := sf.sectorFile.Sync()
			if err != nil {
				wal.cm.log.Severe("ERROR: unable to sync a storage folder:", err)
			}
		}(sf)
	}

	// Sync the temp WAL file, but do not perform the atmoic rename - the
	// atomic rename must be guaranteed to happen after all of the other files
	// have been synced.
	wg.Add(1)
	go func() {
		defer wg.Done()

		err := wal.fileWALTmp.Sync()
		if err != nil {
			wal.cm.log.Severe("Unable to sync the write-ahead-log:", err)
		}
		err = wal.fileWALTmp.Close()
		if err != nil {
			// Log that the host is having trouble saving the uncommitted changes.
			// Crash if the list of uncommitted changes has grown very large.
			wal.cm.log.Println("ERROR: could not close temporary write-ahead-log in contract manager:", err)
			return
		}
	}()

	// Wait for all of the sync calls to finish.
	wg.Wait()

	// Now that all the Sync calls have completed, rename the WAL tmp file to
	// update the WAL.
	if !wal.cm.dependencies.disrupt("walRename") {
		walTmpName := filepath.Join(wal.cm.persistDir, walFileTmp)
		walFileName := filepath.Join(wal.cm.persistDir, walFile)
		err := wal.cm.dependencies.renameFile(walTmpName, walFileName)
		if err != nil {
			// Log that the host is having trouble saving the uncommitted changes.
			// Crash if the list of uncommitted changes has grown very large.
			wal.cm.log.Severe("ERROR: could not rename temporary write-ahead-log in contract manager:", err)
		}
	}

	// Perform any cleanup actions on the updates.
	for _, sc := range wal.uncommittedChanges {
		for _, sfe := range sc.StorageFolderExtensions {
			wal.commitStorageFolderExtension(sfe)
		}
		for _, sfr := range sc.StorageFolderReductions {
			wal.commitStorageFolderReduction(sfr)
		}
		for _, sfr := range sc.StorageFolderRemovals {
			wal.commitStorageFolderRemoval(sfr)
		}

		// TODO: Virtual sector handling here.
	}

	// Now that the WAL is sync'd and updated, any calls waiting on ACID
	// guarantees can safely return.
	close(wal.syncChan)
	wal.syncChan = make(chan struct{})
}

// commit will take all of the changes that have been added to the WAL and
// atomically commit the WAL to disk, then apply the actions in the WAL to the
// state. commit will do lots of syncing disk I/O, and so can take a while,
// especially if there are a large number of actions queued up.
//
// A bool is returned indicating whether or not the commit was successful.
// False does not indiciate an error, it can also indicate that there was
// nothing to do.
//
// commit should only be called from threadedSyncLoop.
func (wal *writeAheadLog) commit() {
	// Sync all open, non-WAL files on the host.
	wal.syncResources()

	// Extract any unfinished long-running jobs from the list of WAL items.
	unfinishedAdditions := findUnfinishedStorageFolderAdditions(wal.uncommittedChanges)
	unfinishedExtensions := findUnfinishedStorageFolderExtensions(wal.uncommittedChanges)

	// Clear the set of uncommitted changes.
	wal.uncommittedChanges = nil

	// Begin writing to the settings file.
	var wg sync.WaitGroup
	wg.Add(1)
	go func() {
		defer wg.Done()

		// Begin writing to the settings file, which will be synced during the
		// next iteration of the sync loop.
		var err error
		wal.fileSettingsTmp, err = wal.cm.dependencies.createFile(filepath.Join(wal.cm.persistDir, settingsFileTmp))
		if err != nil {
			wal.cm.log.Severe("Unable to open temporary settings file for writing:", err)
		}
		ss := wal.cm.savedSettings()
		b, err := json.MarshalIndent(ss, "", "\t")
		if err != nil {
			build.ExtendErr("unable to marshal settings data", err)
		}
		enc := json.NewEncoder(wal.fileSettingsTmp)
		if err := enc.Encode(settingsMetadata.Header); err != nil {
			build.ExtendErr("unable to write header to settings temp file", err)
		}
		if err := enc.Encode(settingsMetadata.Version); err != nil {
			build.ExtendErr("unable to write version to settings temp file", err)
		}
		if _, err = wal.fileSettingsTmp.Write(b); err != nil {
			build.ExtendErr("unable to write data settings temp file", err)
		}
	}()

	// Begin writing new changes to the WAL.
	wg.Add(1)
	go func() {
		defer wg.Done()

		// Recreate the wal file so that it can receive new updates.
		var err error
		walTmpName := filepath.Join(wal.cm.persistDir, walFileTmp)
		wal.fileWALTmp, err = wal.cm.dependencies.createFile(walTmpName)
		if err != nil {
			wal.cm.log.Severe("ERROR: unable to create write-ahead-log:", err)
		}
		// Write the mmetaetadata into the WAL.
		err = writeWALMetadata(wal.fileWALTmp)
		if err != nil {
			wal.cm.log.Severe("Unable to properly initialize WAL file, crashing to prevent corruption:", err)
		}

		// Append all of the remaining long running uncommitted changes to the WAL.
		wal.appendChange(stateChange{
			UnfinishedStorageFolderAdditions:  unfinishedAdditions,
			UnfinishedStorageFolderExtensions: unfinishedExtensions,
		})
	}()
	wg.Wait()
}

// spawnSyncLoop prepares and establishes the loop which will be running in the
// background to coordinate disk syncronizations. Disk syncing is done in a
// background loop to help with performance, and to allow multiple things to
// modify the WAL simultaneously.
func (wal *writeAheadLog) spawnSyncLoop() (err error) {
	// Create a signal so we know when the sync loop has stopped, which means
	// there will be no more open commits.
	threadsStopped := make(chan struct{})
	syncLoopStopped := make(chan struct{})
	wal.syncChan = make(chan struct{})
	go wal.threadedSyncLoop(threadsStopped, syncLoopStopped)
	wal.cm.tg.AfterStop(func() {
		// Wait for another iteration of the sync loop, so that the in-progress
		// settings can be saved atomically to disk.
		wal.mu.Lock()
		syncChan := wal.syncChan
		wal.mu.Unlock()
		<-syncChan

		// Close the threadsStopped channel to let the sync loop know that all
		// calls to tg.Add() in the contract manager have cleaned up.
		close(threadsStopped)

		// Because this is being called in an 'AfterStop' routine, all open
		// calls to the contract manager should have completed, and all open
		// threads should have closed. The last call to change the contract
		// manager should have completed, so the number of uncommitted changes
		// should be zero.
		<-syncLoopStopped // Wait for the sync loop to signal proper termination.

		// Allow unclean shutdown to be simulated by disrupting the removal of
		// the WAL file.
		if !wal.cm.dependencies.disrupt("cleanWALFile") {
			err = wal.cm.dependencies.removeFile(filepath.Join(wal.cm.persistDir, walFile))
			if err != nil {
				wal.cm.log.Println("Error removing WAL during contract manager shutdown:", err)
			}
		}
	})
	return nil
}

// threadedSyncLoop is a background thread that occasionally commits the WAL to
// the state as an ACID transaction. This process can be very slow, so
// transactions to the contract manager are batched automatically and
// occasionally committed together.
func (wal *writeAheadLog) threadedSyncLoop(threadsStopped chan struct{}, syncLoopStopped chan struct{}) {
	// Provide a place for the testing to disable the sync loop.
	if wal.cm.dependencies.disrupt("threadedSyncLoopStart") {
		close(syncLoopStopped)
		return
	}

	syncInterval := 500 * time.Millisecond
	for {
		select {
		case <-threadsStopped:
			close(syncLoopStopped)
			return
		case <-time.After(syncInterval):
			// Commit all of the changes in the WAL to disk, and then apply the
			// changes.
			wal.mu.Lock()
			wal.commit()
			wal.mu.Unlock()
		}
	}
}
<commit_msg>fix comment typo<commit_after>package contractmanager

import (
	"encoding/json"
	"path/filepath"
	"sync"
	"time"

	"github.com/NebulousLabs/Sia/build"
)

// syncResources will call Sync on all resources that the WAL has open. The
// storage folder files will be left open, as they are not updated atomically.
// The settings file and WAL tmp files will be synced and closed, to perform an
// atomic update to the files.
func (wal *writeAheadLog) syncResources() {
	// Syncing occurs over multiple files and disks, and is done in parallel to
	// minimize the amount of time that a lock is held over the contract
	// manager.
	var wg sync.WaitGroup

	// Sync the settings file.
	wg.Add(1)
	go func() {
		defer wg.Done()

		tmpFilename := filepath.Join(wal.cm.persistDir, settingsFileTmp)
		filename := filepath.Join(wal.cm.persistDir, settingsFile)
		err := wal.fileSettingsTmp.Sync()
		if err != nil {
			wal.cm.log.Severe("ERROR: unable to sync the contract manager settings:", err)
		}
		err = wal.fileSettingsTmp.Close()
		if err != nil {
			wal.cm.log.Println("unable to close the temporary contract manager settings file:", err)
		}

		// For testing, provide a place to interrupt the saving of the sync
		// file. This makes it easy to simulate certain types of unclean
		// shutdown.
		if wal.cm.dependencies.disrupt("settingsSyncRename") {
			// The current settings file that is being re-written will not be
			// saved.
			return
		}

		err = wal.cm.dependencies.renameFile(tmpFilename, filename)
		if err != nil {
			wal.cm.log.Severe("ERROR: unable to atomically copy the contract manager settings:", err)
		}
	}()

	// Sync all of the storage folders.
	for _, sf := range wal.cm.storageFolders {
		wg.Add(2)
		go func(sf *storageFolder) {
			defer wg.Done()
			err := sf.metadataFile.Sync()
			if err != nil {
				wal.cm.log.Severe("ERROR: unable to sync a storage folder:", err)
			}
		}(sf)
		go func(sf *storageFolder) {
			defer wg.Done()
			err := sf.sectorFile.Sync()
			if err != nil {
				wal.cm.log.Severe("ERROR: unable to sync a storage folder:", err)
			}
		}(sf)
	}

	// Sync the temp WAL file, but do not perform the atmoic rename - the
	// atomic rename must be guaranteed to happen after all of the other files
	// have been synced.
	wg.Add(1)
	go func() {
		defer wg.Done()

		err := wal.fileWALTmp.Sync()
		if err != nil {
			wal.cm.log.Severe("Unable to sync the write-ahead-log:", err)
		}
		err = wal.fileWALTmp.Close()
		if err != nil {
			// Log that the host is having trouble saving the uncommitted changes.
			// Crash if the list of uncommitted changes has grown very large.
			wal.cm.log.Println("ERROR: could not close temporary write-ahead-log in contract manager:", err)
			return
		}
	}()

	// Wait for all of the sync calls to finish.
	wg.Wait()

	// Now that all the Sync calls have completed, rename the WAL tmp file to
	// update the WAL.
	if !wal.cm.dependencies.disrupt("walRename") {
		walTmpName := filepath.Join(wal.cm.persistDir, walFileTmp)
		walFileName := filepath.Join(wal.cm.persistDir, walFile)
		err := wal.cm.dependencies.renameFile(walTmpName, walFileName)
		if err != nil {
			// Log that the host is having trouble saving the uncommitted changes.
			// Crash if the list of uncommitted changes has grown very large.
			wal.cm.log.Severe("ERROR: could not rename temporary write-ahead-log in contract manager:", err)
		}
	}

	// Perform any cleanup actions on the updates.
	for _, sc := range wal.uncommittedChanges {
		for _, sfe := range sc.StorageFolderExtensions {
			wal.commitStorageFolderExtension(sfe)
		}
		for _, sfr := range sc.StorageFolderReductions {
			wal.commitStorageFolderReduction(sfr)
		}
		for _, sfr := range sc.StorageFolderRemovals {
			wal.commitStorageFolderRemoval(sfr)
		}

		// TODO: Virtual sector handling here.
	}

	// Now that the WAL is sync'd and updated, any calls waiting on ACID
	// guarantees can safely return.
	close(wal.syncChan)
	wal.syncChan = make(chan struct{})
}

// commit will take all of the changes that have been added to the WAL and
// atomically commit the WAL to disk, then apply the actions in the WAL to the
// state. commit will do lots of syncing disk I/O, and so can take a while,
// especially if there are a large number of actions queued up.
//
// A bool is returned indicating whether or not the commit was successful.
// False does not indiciate an error, it can also indicate that there was
// nothing to do.
//
// commit should only be called from threadedSyncLoop.
func (wal *writeAheadLog) commit() {
	// Sync all open, non-WAL files on the host.
	wal.syncResources()

	// Extract any unfinished long-running jobs from the list of WAL items.
	unfinishedAdditions := findUnfinishedStorageFolderAdditions(wal.uncommittedChanges)
	unfinishedExtensions := findUnfinishedStorageFolderExtensions(wal.uncommittedChanges)

	// Clear the set of uncommitted changes.
	wal.uncommittedChanges = nil

	// Begin writing to the settings file.
	var wg sync.WaitGroup
	wg.Add(1)
	go func() {
		defer wg.Done()

		// Begin writing to the settings file, which will be synced during the
		// next iteration of the sync loop.
		var err error
		wal.fileSettingsTmp, err = wal.cm.dependencies.createFile(filepath.Join(wal.cm.persistDir, settingsFileTmp))
		if err != nil {
			wal.cm.log.Severe("Unable to open temporary settings file for writing:", err)
		}
		ss := wal.cm.savedSettings()
		b, err := json.MarshalIndent(ss, "", "\t")
		if err != nil {
			build.ExtendErr("unable to marshal settings data", err)
		}
		enc := json.NewEncoder(wal.fileSettingsTmp)
		if err := enc.Encode(settingsMetadata.Header); err != nil {
			build.ExtendErr("unable to write header to settings temp file", err)
		}
		if err := enc.Encode(settingsMetadata.Version); err != nil {
			build.ExtendErr("unable to write version to settings temp file", err)
		}
		if _, err = wal.fileSettingsTmp.Write(b); err != nil {
			build.ExtendErr("unable to write data settings temp file", err)
		}
	}()

	// Begin writing new changes to the WAL.
	wg.Add(1)
	go func() {
		defer wg.Done()

		// Recreate the wal file so that it can receive new updates.
		var err error
		walTmpName := filepath.Join(wal.cm.persistDir, walFileTmp)
		wal.fileWALTmp, err = wal.cm.dependencies.createFile(walTmpName)
		if err != nil {
			wal.cm.log.Severe("ERROR: unable to create write-ahead-log:", err)
		}
		// Write the metadata into the WAL.
		err = writeWALMetadata(wal.fileWALTmp)
		if err != nil {
			wal.cm.log.Severe("Unable to properly initialize WAL file, crashing to prevent corruption:", err)
		}

		// Append all of the remaining long running uncommitted changes to the WAL.
		wal.appendChange(stateChange{
			UnfinishedStorageFolderAdditions:  unfinishedAdditions,
			UnfinishedStorageFolderExtensions: unfinishedExtensions,
		})
	}()
	wg.Wait()
}

// spawnSyncLoop prepares and establishes the loop which will be running in the
// background to coordinate disk syncronizations. Disk syncing is done in a
// background loop to help with performance, and to allow multiple things to
// modify the WAL simultaneously.
func (wal *writeAheadLog) spawnSyncLoop() (err error) {
	// Create a signal so we know when the sync loop has stopped, which means
	// there will be no more open commits.
	threadsStopped := make(chan struct{})
	syncLoopStopped := make(chan struct{})
	wal.syncChan = make(chan struct{})
	go wal.threadedSyncLoop(threadsStopped, syncLoopStopped)
	wal.cm.tg.AfterStop(func() {
		// Wait for another iteration of the sync loop, so that the in-progress
		// settings can be saved atomically to disk.
		wal.mu.Lock()
		syncChan := wal.syncChan
		wal.mu.Unlock()
		<-syncChan

		// Close the threadsStopped channel to let the sync loop know that all
		// calls to tg.Add() in the contract manager have cleaned up.
		close(threadsStopped)

		// Because this is being called in an 'AfterStop' routine, all open
		// calls to the contract manager should have completed, and all open
		// threads should have closed. The last call to change the contract
		// manager should have completed, so the number of uncommitted changes
		// should be zero.
		<-syncLoopStopped // Wait for the sync loop to signal proper termination.

		// Allow unclean shutdown to be simulated by disrupting the removal of
		// the WAL file.
		if !wal.cm.dependencies.disrupt("cleanWALFile") {
			err = wal.cm.dependencies.removeFile(filepath.Join(wal.cm.persistDir, walFile))
			if err != nil {
				wal.cm.log.Println("Error removing WAL during contract manager shutdown:", err)
			}
		}
	})
	return nil
}

// threadedSyncLoop is a background thread that occasionally commits the WAL to
// the state as an ACID transaction. This process can be very slow, so
// transactions to the contract manager are batched automatically and
// occasionally committed together.
func (wal *writeAheadLog) threadedSyncLoop(threadsStopped chan struct{}, syncLoopStopped chan struct{}) {
	// Provide a place for the testing to disable the sync loop.
	if wal.cm.dependencies.disrupt("threadedSyncLoopStart") {
		close(syncLoopStopped)
		return
	}

	syncInterval := 500 * time.Millisecond
	for {
		select {
		case <-threadsStopped:
			close(syncLoopStopped)
			return
		case <-time.After(syncInterval):
			// Commit all of the changes in the WAL to disk, and then apply the
			// changes.
			wal.mu.Lock()
			wal.commit()
			wal.mu.Unlock()
		}
	}
}
<|endoftext|>


<commit_before>// Package pages provides a data structure for a web pages.
package pages

import (
	"bytes"
	"encoding/base64"
	"errors"
	"fmt"
	"log"
	"net/http"
	"net/url"
	"path"

	"github.com/ChristianSiegert/go-packages/chttp"
	"github.com/ChristianSiegert/go-packages/forms"
	"github.com/ChristianSiegert/go-packages/html"
	"github.com/ChristianSiegert/go-packages/i18n/languages"
	"github.com/ChristianSiegert/go-packages/sessions"
	"golang.org/x/net/context"
)

// Whether NewPage and MustNewPage should reload the provided template.
// Reloading templates on each request is useful to see changes without
// recompiling. In production, reloading should be disabled.
var ReloadTemplates = false

// Path to template that is used as root template when
// Page.Serve[Empty|NotFound|Unauthorized|WithError] is called.
var RootTemplatePath = "./templates/index.html"

// Templates that are used as content template when Page.ServeEmpty or
// Page.ServeNotFound is called.
var (
	TemplateEmpty    = MustNewTemplate("./templates/empty.html", nil)
	TemplateNotFound = MustNewTemplate("./templates/404-not-found.html", nil)
)

// Template that is used as content template when Page.Error is called.
var TemplateError = MustNewTemplateWithRoot("./templates/error.html", "./templates/500-internal-server-error.html", nil)

// SignInUrl is the URL to the page that users are redirected to when
// Page.RequireSignIn is called. If a %s placeholder is present in
// SignInUrl.Path, it is replaced by the page’s language code. E.g.
// “/%s/sign-in” becomes “/en/sign-in” if the page’s language code is “en”.
var SignInUrl = &url.URL{
	Path: "/%s/sign-in",
}

// Page represents a web page.
type Page struct {
	Breadcrumbs []*Breadcrumb

	Data map[string]interface{}

	// Form is an instance of *forms.Form bound to the request.
	Form *forms.Form

	Language *languages.Language

	// Name of the page. Useful in the root template, e.g. to style the
	// navigation link of the current page.
	Name string

	request *http.Request

	responseWriter http.ResponseWriter

	Session *sessions.Session

	Template *Template

	// Title of the page that templates can use to populate the HTML <title>
	// element.
	Title string
}

func NewPage(ctx context.Context, tpl *Template) (*Page, error) {
	responseWriter, request, ok := chttp.FromContext(ctx)
	if !ok {
		return nil, errors.New("pages.NewPage: http.ResponseWriter and http.Request are not provided by ctx.")
	}

	language, ok := languages.FromContext(ctx)
	if !ok {
		return nil, errors.New("pages.NewPage: languages.Language is not provided by ctx.")
	}

	session, ok := sessions.FromContext(ctx)
	if !ok {
		return nil, errors.New("pages.NewPage: sessions.Session is not provided by ctx.")
	}

	form, err := forms.New(ctx)
	if err != nil {
		return nil, err
	}

	page := &Page{
		Form:           form,
		Language:       language,
		request:        request,
		responseWriter: responseWriter,
		Session:        session,
		Template:       tpl,
	}

	return page, nil
}

// MustNewPage calls NewPage and panics on error.
func MustNewPage(ctx context.Context, tpl *Template) *Page {
	page, err := NewPage(ctx, tpl)
	if err != nil {
		panic("pages.MustNewPage: " + err.Error())
	}
	return page
}

func (p *Page) AddBreadcrumb(title string, url *url.URL) *Breadcrumb {
	breadcrumb := &Breadcrumb{
		Title: title,
		Url:   url,
	}

	p.Breadcrumbs = append(p.Breadcrumbs, breadcrumb)
	return breadcrumb
}

// Redirect redirects the client.
func (p *Page) Redirect(urlStr string, code int) {
	http.Redirect(p.responseWriter, p.request, urlStr, code)
}

// RequireSignIn redirects users to the sign-in page specified by SignInUrl.
// If SignInUrl.RawQuery is empty, the query parameters “r” (referrer) and “t”
// (title of the referrer page) are appended. This allows the sign-in page to
// display a message that page <title> is access restricted, and after
// successful authentication, users can be redirected to <referrer>, the page
// they came from.
func (p *Page) RequireSignIn(pageTitle string) {
	u := &url.URL{
		Scheme:   SignInUrl.Scheme,
		Opaque:   SignInUrl.Opaque,
		User:     SignInUrl.User,
		Host:     SignInUrl.Host,
		Path:     fmt.Sprintf(SignInUrl.Path, p.Language.Code()),
		Fragment: SignInUrl.Fragment,
	}

	if SignInUrl.RawQuery == "" {
		query := &url.Values{}
		query.Add("r", p.request.URL.Path)
		query.Add("t", base64.URLEncoding.EncodeToString([]byte(pageTitle))) // TODO: Sign or encrypt parameter to prevent tempering by users
		u.RawQuery = query.Encode()
	}

	p.Redirect(u.String(), http.StatusSeeOther)
}

// Error serves an error page with a generic error message. Err is not displayed
// to the user but written to the error log.
func (p *Page) Error(err error) {
	log.Println(err.Error())

	if TemplateError == nil {
		log.Println("pages.Page.Error: TemplateError is nil.")
		http.Error(p.responseWriter, "Internal Server Error", http.StatusInternalServerError)
		return
	}

	buffer := bytes.NewBuffer([]byte{})

	p.Data = map[string]interface{}{
		"Error":          err,
		"IsDevAppServer": true,
	}

	if ReloadTemplates {
		if err := p.Template.Reload(); err != nil {
			log.Printf("pages.Page.Error: Reloading template failed:\n%s\n%s\n", p.Template.rootTemplatePath, p.Template.contentTemplatePath)
			http.Error(p.responseWriter, "Internal Server Error", http.StatusInternalServerError)
		}
	}

	if err := TemplateError.template.ExecuteTemplate(buffer, path.Base(TemplateError.rootTemplatePath), p); err != nil {
		log.Printf("pages.Page.Error: Executing template failed: %s\n", err)
		http.Error(p.responseWriter, "Internal Server Error", http.StatusInternalServerError)
		return
	}

	b := html.RemoveWhitespace(buffer.Bytes())

	if _, err := bytes.NewBuffer(b).WriteTo(p.responseWriter); err != nil {
		log.Printf("pages.Page.Error: Writing template to buffer failed: %s\n", err)
		http.Error(p.responseWriter, "Internal Server Error", http.StatusInternalServerError)
	}
}

// Serve serves the root template specified by RootTemplatePath with the content
// template specified by p.Template. HTML comments and whitespace are stripped.
// If p.Template is nil, an empty content template is embedded.
func (p *Page) Serve() {
	buffer := bytes.NewBuffer([]byte{})

	if p.Template == nil {
		p.Template = TemplateEmpty
	}

	// If still nil
	if p.Template == nil {
		p.Error(errors.New("pages.Page.Serve: p.Template is nil."))
		return
	}

	if ReloadTemplates {
		if err := p.Template.Reload(); err != nil {
			p.Error(err)
		}
	}

	templateName := path.Base(p.Template.rootTemplatePath)
	if err := p.Template.template.ExecuteTemplate(buffer, templateName, p); err != nil {
		p.Error(err)
		return
	}

	b := html.RemoveWhitespace(buffer.Bytes())

	if _, err := bytes.NewBuffer(b).WriteTo(p.responseWriter); err != nil {
		p.Error(err)
	}
}

// ServeEmpty serves the root template without content template.
func (p *Page) ServeEmpty() {
	p.Template = TemplateEmpty
	p.Serve()
}

// ServeNotFound serves a page that tells the user the requested page does not
// exist.
func (p *Page) ServeNotFound() {
	p.responseWriter.WriteHeader(http.StatusNotFound)
	p.Template = TemplateNotFound
	p.Title = p.T("err_page_not_found")
	p.Serve()
}

// ServeUnauthorized serves a page that tells the user the requested page cannot
// be accessed due to insufficient access rights.
func (p *Page) ServeUnauthorized() {
	p.Session.AddFlashError(p.T("err_unauthorized_access"))
	p.responseWriter.WriteHeader(http.StatusUnauthorized)
	p.ServeEmpty()
}

// ServeWithError is similar to Serve, but additionally an error flash message
// is displayed to the user saying that an internal problem occurred. Err is not
// displayed but written to the error log. This method is useful if the user
// should be informed of a problem while the state, e.g. a filled in form, is
// preserved.
func (p *Page) ServeWithError(err error) {
	log.Println(err.Error())
	p.Session.AddFlashError(p.T("err_internal_server_error"))
	p.Serve()
}

// T returns the translation associated with translationId. If p.Language
// is nil, translationId is returned.
func (p *Page) T(translationId string, templateData ...map[string]interface{}) string {
	if p.Language == nil {
		return translationId
	}
	return p.Language.T(translationId, templateData...)
}

func Error(ctx context.Context, err error) {
	page := MustNewPage(ctx, TemplateError)
	page.Error(err)
}
<commit_msg>Renamed variable.<commit_after>// Package pages provides a data structure for a web pages.
package pages

import (
	"bytes"
	"encoding/base64"
	"errors"
	"fmt"
	"log"
	"net/http"
	"net/url"
	"path"

	"github.com/ChristianSiegert/go-packages/chttp"
	"github.com/ChristianSiegert/go-packages/forms"
	"github.com/ChristianSiegert/go-packages/html"
	"github.com/ChristianSiegert/go-packages/i18n/languages"
	"github.com/ChristianSiegert/go-packages/sessions"
	"golang.org/x/net/context"
)

// Whether NewPage and MustNewPage should reload the provided template.
// Reloading templates on each request is useful to see changes without
// recompiling. In production, reloading should be disabled.
var ReloadTemplates = false

// Path to template that is used as root template when
// Page.Serve[Empty|NotFound|Unauthorized|WithError] is called.
var RootTemplatePath = "./templates/index.html"

// Templates that are used as content template when Page.ServeEmpty or
// Page.ServeNotFound is called.
var (
	TemplateEmpty    = MustNewTemplate("./templates/empty.html", nil)
	TemplateNotFound = MustNewTemplate("./templates/404-not-found.html", nil)
)

// Template that is used as content template when Page.Error is called.
var TemplateError = MustNewTemplateWithRoot("./templates/error.html", "./templates/500-internal-server-error.html", nil)

// SignInUrl is the URL to the page that users are redirected to when
// Page.RequireSignIn is called. If a %s placeholder is present in
// SignInUrl.Path, it is replaced by the page’s language code. E.g.
// “/%s/sign-in” becomes “/en/sign-in” if the page’s language code is “en”.
var SignInUrl = &url.URL{
	Path: "/%s/sign-in",
}

// Page represents a web page.
type Page struct {
	Breadcrumbs []*Breadcrumb

	Data map[string]interface{}

	// Form is an instance of *forms.Form bound to the request.
	Form *forms.Form

	Language *languages.Language

	// Name of the page. Useful in the root template, e.g. to style the
	// navigation link of the current page.
	Name string

	request *http.Request

	responseWriter http.ResponseWriter

	Session *sessions.Session

	Template *Template

	// Title of the page that templates can use to populate the HTML <title>
	// element.
	Title string
}

func NewPage(ctx context.Context, tpl *Template) (*Page, error) {
	responseWriter, request, ok := chttp.FromContext(ctx)
	if !ok {
		return nil, errors.New("pages.NewPage: http.ResponseWriter and http.Request are not provided by ctx.")
	}

	language, ok := languages.FromContext(ctx)
	if !ok {
		return nil, errors.New("pages.NewPage: languages.Language is not provided by ctx.")
	}

	session, ok := sessions.FromContext(ctx)
	if !ok {
		return nil, errors.New("pages.NewPage: sessions.Session is not provided by ctx.")
	}

	form, err := forms.New(ctx)
	if err != nil {
		return nil, err
	}

	page := &Page{
		Form:           form,
		Language:       language,
		request:        request,
		responseWriter: responseWriter,
		Session:        session,
		Template:       tpl,
	}

	return page, nil
}

// MustNewPage calls NewPage and panics on error.
func MustNewPage(ctx context.Context, tpl *Template) *Page {
	page, err := NewPage(ctx, tpl)
	if err != nil {
		panic("pages.MustNewPage: " + err.Error())
	}
	return page
}

func (p *Page) AddBreadcrumb(title string, url *url.URL) *Breadcrumb {
	breadcrumb := &Breadcrumb{
		Title: title,
		Url:   url,
	}

	p.Breadcrumbs = append(p.Breadcrumbs, breadcrumb)
	return breadcrumb
}

// Redirect redirects the client.
func (p *Page) Redirect(url string, code int) {
	http.Redirect(p.responseWriter, p.request, url, code)
}

// RequireSignIn redirects users to the sign-in page specified by SignInUrl.
// If SignInUrl.RawQuery is empty, the query parameters “r” (referrer) and “t”
// (title of the referrer page) are appended. This allows the sign-in page to
// display a message that page <title> is access restricted, and after
// successful authentication, users can be redirected to <referrer>, the page
// they came from.
func (p *Page) RequireSignIn(pageTitle string) {
	u := &url.URL{
		Scheme:   SignInUrl.Scheme,
		Opaque:   SignInUrl.Opaque,
		User:     SignInUrl.User,
		Host:     SignInUrl.Host,
		Path:     fmt.Sprintf(SignInUrl.Path, p.Language.Code()),
		Fragment: SignInUrl.Fragment,
	}

	if SignInUrl.RawQuery == "" {
		query := &url.Values{}
		query.Add("r", p.request.URL.Path)
		query.Add("t", base64.URLEncoding.EncodeToString([]byte(pageTitle))) // TODO: Sign or encrypt parameter to prevent tempering by users
		u.RawQuery = query.Encode()
	}

	p.Redirect(u.String(), http.StatusSeeOther)
}

// Error serves an error page with a generic error message. Err is not displayed
// to the user but written to the error log.
func (p *Page) Error(err error) {
	log.Println(err.Error())

	if TemplateError == nil {
		log.Println("pages.Page.Error: TemplateError is nil.")
		http.Error(p.responseWriter, "Internal Server Error", http.StatusInternalServerError)
		return
	}

	buffer := bytes.NewBuffer([]byte{})

	p.Data = map[string]interface{}{
		"Error":          err,
		"IsDevAppServer": true,
	}

	if ReloadTemplates {
		if err := p.Template.Reload(); err != nil {
			log.Printf("pages.Page.Error: Reloading template failed:\n%s\n%s\n", p.Template.rootTemplatePath, p.Template.contentTemplatePath)
			http.Error(p.responseWriter, "Internal Server Error", http.StatusInternalServerError)
		}
	}

	if err := TemplateError.template.ExecuteTemplate(buffer, path.Base(TemplateError.rootTemplatePath), p); err != nil {
		log.Printf("pages.Page.Error: Executing template failed: %s\n", err)
		http.Error(p.responseWriter, "Internal Server Error", http.StatusInternalServerError)
		return
	}

	b := html.RemoveWhitespace(buffer.Bytes())

	if _, err := bytes.NewBuffer(b).WriteTo(p.responseWriter); err != nil {
		log.Printf("pages.Page.Error: Writing template to buffer failed: %s\n", err)
		http.Error(p.responseWriter, "Internal Server Error", http.StatusInternalServerError)
	}
}

// Serve serves the root template specified by RootTemplatePath with the content
// template specified by p.Template. HTML comments and whitespace are stripped.
// If p.Template is nil, an empty content template is embedded.
func (p *Page) Serve() {
	buffer := bytes.NewBuffer([]byte{})

	if p.Template == nil {
		p.Template = TemplateEmpty
	}

	// If still nil
	if p.Template == nil {
		p.Error(errors.New("pages.Page.Serve: p.Template is nil."))
		return
	}

	if ReloadTemplates {
		if err := p.Template.Reload(); err != nil {
			p.Error(err)
		}
	}

	templateName := path.Base(p.Template.rootTemplatePath)
	if err := p.Template.template.ExecuteTemplate(buffer, templateName, p); err != nil {
		p.Error(err)
		return
	}

	b := html.RemoveWhitespace(buffer.Bytes())

	if _, err := bytes.NewBuffer(b).WriteTo(p.responseWriter); err != nil {
		p.Error(err)
	}
}

// ServeEmpty serves the root template without content template.
func (p *Page) ServeEmpty() {
	p.Template = TemplateEmpty
	p.Serve()
}

// ServeNotFound serves a page that tells the user the requested page does not
// exist.
func (p *Page) ServeNotFound() {
	p.responseWriter.WriteHeader(http.StatusNotFound)
	p.Template = TemplateNotFound
	p.Title = p.T("err_page_not_found")
	p.Serve()
}

// ServeUnauthorized serves a page that tells the user the requested page cannot
// be accessed due to insufficient access rights.
func (p *Page) ServeUnauthorized() {
	p.Session.AddFlashError(p.T("err_unauthorized_access"))
	p.responseWriter.WriteHeader(http.StatusUnauthorized)
	p.ServeEmpty()
}

// ServeWithError is similar to Serve, but additionally an error flash message
// is displayed to the user saying that an internal problem occurred. Err is not
// displayed but written to the error log. This method is useful if the user
// should be informed of a problem while the state, e.g. a filled in form, is
// preserved.
func (p *Page) ServeWithError(err error) {
	log.Println(err.Error())
	p.Session.AddFlashError(p.T("err_internal_server_error"))
	p.Serve()
}

// T returns the translation associated with translationId. If p.Language
// is nil, translationId is returned.
func (p *Page) T(translationId string, templateData ...map[string]interface{}) string {
	if p.Language == nil {
		return translationId
	}
	return p.Language.T(translationId, templateData...)
}

func Error(ctx context.Context, err error) {
	page := MustNewPage(ctx, TemplateError)
	page.Error(err)
}
<|endoftext|>


<commit_before>package terraformrules

import (
	"testing"

	hcl "github.com/hashicorp/hcl/v2"
	"github.com/terraform-linters/tflint/tflint"
)

func Test_TerraformWorkspaceRemoteRule(t *testing.T) {
	cases := []struct {
		Name     string
		Content  string
		Expected tflint.Issues
	}{
		{
			Name: "terraform.workspace in resource with remote backend",
			Content: `
terraform {
	backend "remote" {}
}
resource "null_resource" "a" {
	triggers = {
		w = terraform.workspace
	}
}`,
			Expected: tflint.Issues{
				{
					Rule:    NewTerraformDeprecatedInterpolationRule(),
					Message: "terraform.workspace should not be used with a 'remote' backend",
					Range: hcl.Range{
						Filename: "config.tf",
						Start:    hcl.Pos{Line: 6, Column: 13},
						End:      hcl.Pos{Line: 8, Column: 3},
					},
				},
			},
		},
		{
			Name: "terraform.workspace with no backend",
			Content: `
resource "null_resource" "a" {
	triggers = {
		w = terraform.workspace
	}
}`,
			Expected: tflint.Issues{},
		},
		{
			Name: "terraform.workspace with non-remote backend",
			Content: `
terraform {
	backend "local" {}
}

resource "null_resource" "a" {
	triggers = {
		w = terraform.workspace
	}
}`,
			Expected: tflint.Issues{},
		},
		{
			Name: "terraform.workspace in data source with remote backend",
			Content: `
terraform {
	backend "remote" {}
}
data "null_data_source" "a" {
	inputs = {
		w = terraform.workspace
	}
}`,
			Expected: tflint.Issues{
				{
					Rule:    NewTerraformDeprecatedInterpolationRule(),
					Message: "terraform.workspace should not be used with a 'remote' backend",
					Range: hcl.Range{
						Filename: "config.tf",
						Start:    hcl.Pos{Line: 6, Column: 11},
						End:      hcl.Pos{Line: 8, Column: 3},
					},
				},
			},
		},
		{
			Name: "terraform.workspace in module call with remote backend",
			Content: `
terraform {
	backend "remote" {}
}
module "a" {
	source = "."
	w = terraform.workspace
}`,
			Expected: tflint.Issues{
				{
					Rule:    NewTerraformDeprecatedInterpolationRule(),
					Message: "terraform.workspace should not be used with a 'remote' backend",
					Range: hcl.Range{
						Filename: "config.tf",
						Start:    hcl.Pos{Line: 7, Column: 6},
						End:      hcl.Pos{Line: 7, Column: 25},
					},
				},
			},
		},
		{
			Name: "terraform.workspace in provider config with remote backend",
			Content: `
terraform {
	backend "remote" {}
}
provider "aws" {
	assume_role	{
		role_arn = terraform.workspace
	}
}`,
			Expected: tflint.Issues{
				{
					Rule:    NewTerraformDeprecatedInterpolationRule(),
					Message: "terraform.workspace should not be used with a 'remote' backend",
					Range: hcl.Range{
						Filename: "config.tf",
						Start:    hcl.Pos{Line: 7, Column: 14},
						End:      hcl.Pos{Line: 7, Column: 33},
					},
				},
			},
		},
		{
			Name: "terraform.workspace in locals with remote backend",
			Content: `
terraform {
	backend "remote" {}
}
locals {
	w = terraform.workspace
}`,
			Expected: tflint.Issues{
				{
					Rule:    NewTerraformDeprecatedInterpolationRule(),
					Message: "terraform.workspace should not be used with a 'remote' backend",
					Range: hcl.Range{
						Filename: "config.tf",
						Start:    hcl.Pos{Line: 6, Column: 6},
						End:      hcl.Pos{Line: 6, Column: 25},
					},
				},
			},
		},
		{
			Name: "terraform.workspace in output with remote backend",
			Content: `
terraform {
	backend "remote" {}
}
output "o" {
	value = terraform.workspace
}`,
			Expected: tflint.Issues{
				{
					Rule:    NewTerraformDeprecatedInterpolationRule(),
					Message: "terraform.workspace should not be used with a 'remote' backend",
					Range: hcl.Range{
						Filename: "config.tf",
						Start:    hcl.Pos{Line: 6, Column: 10},
						End:      hcl.Pos{Line: 6, Column: 29},
					},
				},
			},
		},
		{
			Name: "nonmatching expressions with remote backend",
			Content: `
terraform {
	backend "remote" {}
}
locals {
	a = "terraform.workspace"
	b = path.module
}`,
			Expected: tflint.Issues{},
		},
	}

	rule := NewTerraformWorkspaceRemoteRule()

	for _, tc := range cases {
		t.Run(tc.Name, func(t *testing.T) {
			runner := tflint.TestRunner(t, map[string]string{"config.tf": tc.Content})

			if err := rule.Check(runner); err != nil {
				t.Fatalf("Unexpected error occurred: %s", err)
			}

			tflint.AssertIssues(t, tc.Expected, runner.Issues)
		})
	}
}
<commit_msg>fix rules in tests<commit_after>package terraformrules

import (
	"testing"

	hcl "github.com/hashicorp/hcl/v2"
	"github.com/terraform-linters/tflint/tflint"
)

func Test_TerraformWorkspaceRemoteRule(t *testing.T) {
	cases := []struct {
		Name     string
		Content  string
		Expected tflint.Issues
	}{
		{
			Name: "terraform.workspace in resource with remote backend",
			Content: `
terraform {
	backend "remote" {}
}
resource "null_resource" "a" {
	triggers = {
		w = terraform.workspace
	}
}`,
			Expected: tflint.Issues{
				{
					Rule:    NewTerraformWorkspaceRemoteRule(),
					Message: "terraform.workspace should not be used with a 'remote' backend",
					Range: hcl.Range{
						Filename: "config.tf",
						Start:    hcl.Pos{Line: 6, Column: 13},
						End:      hcl.Pos{Line: 8, Column: 3},
					},
				},
			},
		},
		{
			Name: "terraform.workspace with no backend",
			Content: `
resource "null_resource" "a" {
	triggers = {
		w = terraform.workspace
	}
}`,
			Expected: tflint.Issues{},
		},
		{
			Name: "terraform.workspace with non-remote backend",
			Content: `
terraform {
	backend "local" {}
}

resource "null_resource" "a" {
	triggers = {
		w = terraform.workspace
	}
}`,
			Expected: tflint.Issues{},
		},
		{
			Name: "terraform.workspace in data source with remote backend",
			Content: `
terraform {
	backend "remote" {}
}
data "null_data_source" "a" {
	inputs = {
		w = terraform.workspace
	}
}`,
			Expected: tflint.Issues{
				{
					Rule:    NewTerraformWorkspaceRemoteRule(),
					Message: "terraform.workspace should not be used with a 'remote' backend",
					Range: hcl.Range{
						Filename: "config.tf",
						Start:    hcl.Pos{Line: 6, Column: 11},
						End:      hcl.Pos{Line: 8, Column: 3},
					},
				},
			},
		},
		{
			Name: "terraform.workspace in module call with remote backend",
			Content: `
terraform {
	backend "remote" {}
}
module "a" {
	source = "."
	w = terraform.workspace
}`,
			Expected: tflint.Issues{
				{
					Rule:    NewTerraformWorkspaceRemoteRule(),
					Message: "terraform.workspace should not be used with a 'remote' backend",
					Range: hcl.Range{
						Filename: "config.tf",
						Start:    hcl.Pos{Line: 7, Column: 6},
						End:      hcl.Pos{Line: 7, Column: 25},
					},
				},
			},
		},
		{
			Name: "terraform.workspace in provider config with remote backend",
			Content: `
terraform {
	backend "remote" {}
}
provider "aws" {
	assume_role	{
		role_arn = terraform.workspace
	}
}`,
			Expected: tflint.Issues{
				{
					Rule:    NewTerraformWorkspaceRemoteRule(),
					Message: "terraform.workspace should not be used with a 'remote' backend",
					Range: hcl.Range{
						Filename: "config.tf",
						Start:    hcl.Pos{Line: 7, Column: 14},
						End:      hcl.Pos{Line: 7, Column: 33},
					},
				},
			},
		},
		{
			Name: "terraform.workspace in locals with remote backend",
			Content: `
terraform {
	backend "remote" {}
}
locals {
	w = terraform.workspace
}`,
			Expected: tflint.Issues{
				{
					Rule:    NewTerraformWorkspaceRemoteRule(),
					Message: "terraform.workspace should not be used with a 'remote' backend",
					Range: hcl.Range{
						Filename: "config.tf",
						Start:    hcl.Pos{Line: 6, Column: 6},
						End:      hcl.Pos{Line: 6, Column: 25},
					},
				},
			},
		},
		{
			Name: "terraform.workspace in output with remote backend",
			Content: `
terraform {
	backend "remote" {}
}
output "o" {
	value = terraform.workspace
}`,
			Expected: tflint.Issues{
				{
					Rule:    NewTerraformWorkspaceRemoteRule(),
					Message: "terraform.workspace should not be used with a 'remote' backend",
					Range: hcl.Range{
						Filename: "config.tf",
						Start:    hcl.Pos{Line: 6, Column: 10},
						End:      hcl.Pos{Line: 6, Column: 29},
					},
				},
			},
		},
		{
			Name: "nonmatching expressions with remote backend",
			Content: `
terraform {
	backend "remote" {}
}
locals {
	a = "terraform.workspace"
	b = path.module
}`,
			Expected: tflint.Issues{},
		},
	}

	rule := NewTerraformWorkspaceRemoteRule()

	for _, tc := range cases {
		t.Run(tc.Name, func(t *testing.T) {
			runner := tflint.TestRunner(t, map[string]string{"config.tf": tc.Content})

			if err := rule.Check(runner); err != nil {
				t.Fatalf("Unexpected error occurred: %s", err)
			}

			tflint.AssertIssues(t, tc.Expected, runner.Issues)
		})
	}
}
<|endoftext|>