<commit_before>package rundeck

import (
	"encoding/xml"
	"fmt"
	"strings"
)

// JobSummary is an abbreviated description of a job that includes only its basic
// descriptive information and identifiers.
type JobSummary struct {
	XMLName     xml.Name `xml:"job"`
	ID          string   `xml:"id,attr"`
	Name        string   `xml:"name"`
	GroupName   string   `xml:"group"`
	ProjectName string   `xml:"project"`
	Description string   `xml:"description,omitempty"`
}

type jobSummaryList struct {
	XMLName xml.Name     `xml:"jobs"`
	Jobs    []JobSummary `xml:"job"`
}

// JobDetail is a comprehensive description of a job, including its entire definition.
type JobDetail struct {
	XMLName                   xml.Name            `xml:"job"`
	ID                        string              `xml:"uuid,omitempty"`
	Name                      string              `xml:"name"`
	GroupName                 string              `xml:"group,omitempty"`
	ProjectName               string              `xml:"context>project,omitempty"`
	OptionsConfig             *JobOptions         `xml:"context>options,omitempty"`
	Description               string              `xml:"description,omitempty"`
	LogLevel                  string              `xml:"loglevel,omitempty"`
	AllowConcurrentExecutions bool                `xml:"multipleExecutions"`
	Dispatch                  *JobDispatch        `xml:"dispatch"`
	CommandSequence           *JobCommandSequence `xml:"sequence,omitempty"`
	NodeFilter                *JobNodeFilter      `xml:"nodefilters,omitempty"`
}

type jobDetailList struct {
	XMLName xml.Name    `xml:"joblist"`
	Jobs    []JobDetail `xml:"job"`
}

// JobOptions represents the set of options on a job, if any.
type JobOptions struct {
	PreserveOrder bool        `xml:"preserveOrder,attr,omitempty"`
	Options       []JobOption `xml:"option"`
}

// JobOption represents a single option on a job.
type JobOption struct {
	XMLName                 xml.Name        `xml:"option"`

	// The name of the option, which can be used to interpolate its value
	// into job commands.
	Name                    string          `xml:"name,attr,omitempty"`

	// The default value of the option.
	DefaultValue            string          `xml:"value,attr,omitempty"`

	// A sequence of predefined choices for this option. Mutually exclusive with ValueChoicesURL.
	ValueChoices            JobValueChoices `xml:"values,attr"`

	// A URL from which the predefined choices for this option will be retrieved.
	// Mutually exclusive with ValueChoices
	ValueChoicesURL         string          `xml:"valuesUrl,attr,omitempty"`

	// If set, Rundeck will reject values that are not in the set of predefined choices.
	RequirePredefinedChoice bool            `xml:"enforcedvalues,attr,omitempty"`

	// Regular expression to be used to validate the option value.
	ValidationRegex         string          `xml:"regex,attr,omitempty"`

	// Description of the value to be shown in the Rundeck UI.
	Description             string          `xml:"description,omitempty"`

	// If set, Rundeck requires a value to be set for this option.
	IsRequired              bool            `xml:"required,attr,omitempty"`

	// When either ValueChoices or ValueChoicesURL is set, controls whether more than one
	// choice may be selected as the value.
	AllowsMultipleValues    bool            `xml:"multivalued,attr,omitempty"`

	// If AllowsMultipleChoices is set, the string that will be used to delimit the multiple
	// chosen options.
	MultiValueDelimiter     string          `xml:"delimeter,attr,omitempty"`

	// If set, the input for this field will be obscured in the UI. Useful for passwords
	// and other secrets.
	ObscureInput            bool            `xml:"secure,attr,omitempty"`

	// If set, the value can be accessed from scripts.
	ValueIsExposedToScripts bool            `xml:"valueExposed,attr,omitempty"`
}

// JobValueChoices is a specialization of []string representing a sequence of predefined values
// for a job option.
type JobValueChoices []string

// JobCommandSequence describes the sequence of operations that a job will perform.
type JobCommandSequence struct {
	XMLName          xml.Name     `xml:"sequence"`

	// If set, Rundeck will continue with subsequent commands after a command fails.
	ContinueOnError  bool         `xml:"keepgoing,attr"`

	// Chooses the strategy by which Rundeck will execute commands. Can either be "node-first" or
	// "step-first".
	OrderingStrategy string       `xml:"strategy,attr,omitempty"`

	// Sequence of commands to run in the sequence.
	Commands         []JobCommand `xml:"command"`
}

// JobCommand describes a particular command to run within the sequence of commands on a job.
// The members of this struct are mutually-exclusive except for the pair of ScriptFile and
// ScriptFileArgs.
type JobCommand struct {
	XMLName        xml.Name

	// A literal shell command to run.
	ShellCommand   string            `xml:"exec,omitempty"`

	// An inline program to run. This will be written to disk and executed, so if it is
	// a shell script it should have an appropriate #! line.
	Script         string            `xml:"script,omitempty"`

	// A pre-existing file (on the target nodes) that will be executed.
	ScriptFile     string            `xml:"scriptfile,omitempty"`

	// When ScriptFile is set, the arguments to provide to the script when executing it.
	ScriptFileArgs string            `xml:"scriptargs,omitempty"`

	// A reference to another job to run as this command.
	Job            *JobCommandJobRef `xml:"jobref"`

	// Configuration for a step plugin to run as this command.
	StepPlugin     *JobPlugin        `xml:"step-plugin"`

	// Configuration for a node step plugin to run as this command.
	NodeStepPlugin *JobPlugin        `xml:"node-step-plugin"`
}

// JobCommandJobRef is a reference to another job that will run as one of the commands of a job.
type JobCommandJobRef struct {
	XMLName        xml.Name                  `xml:"jobref"`
	Name           string                    `xml:"name,attr"`
	GroupName      string                    `xml:"group,attr"`
	RunForEachNode bool                      `xml:"nodeStep,attr"`
	Arguments      JobCommandJobRefArguments `xml:"arg"`
}

// JobCommandJobRefArguments is a string representing the arguments in a JobCommandJobRef.
type JobCommandJobRefArguments string

// JobPlugin is a configuration for a plugin to run within a job.
type JobPlugin struct {
	XMLName xml.Name
	Type    string          `xml:"type,attr"`
	Config  JobPluginConfig `xml:"configuration"`
}

// JobPluginConfig is a specialization of map[string]string for job plugin configuration.
type JobPluginConfig map[string]string

// JobNodeFilter describes which nodes from the project's resource list will run the configured
// commands.
type JobNodeFilter struct {
	ExcludePrecedence bool   `xml:"excludeprecedence"`
	Query             string `xml:"filter,omitempty"`
}

type jobImportResults struct {
	Succeeded jobImportResultsCategory `xml:"succeeded"`
	Failed    jobImportResultsCategory `xml:"failed"`
	Skipped   jobImportResultsCategory `xml:"skipped"`
}

type jobImportResultsCategory struct {
	Count   int               `xml:"count,attr"`
	Results []jobImportResult `xml:"job"`
}

type jobImportResult struct {
	ID          string `xml:"id,omitempty"`
	Name        string `xml:"name"`
	GroupName   string `xml:"group,omitempty"`
	ProjectName string `xml:"context>project,omitempty"`
	Error       string `xml:"error"`
}

type JobDispatch struct {
	MaxThreadCount  int    `xml:"threadcount,omitempty"`
	ContinueOnError bool   `xml:"keepgoing"`
	RankAttribute   string `xml:"rankAttribute,omitempty"`
	RankOrder       string `xml:"rankOrder,omitempty"`
}

// GetJobSummariesForProject returns summaries of the jobs belonging to the named project.
func (c *Client) GetJobSummariesForProject(projectName string) ([]JobSummary, error) {
	jobList := &jobSummaryList{}
	err := c.get([]string{"project", projectName, "jobs"}, nil, jobList)
	return jobList.Jobs, err
}

// GetJobsForProject returns the full job details of the jobs belonging to the named project.
func (c *Client) GetJobsForProject(projectName string) ([]JobDetail, error) {
	jobList := &jobDetailList{}
	err := c.get([]string{"jobs", "export"}, map[string]string{"project": projectName}, jobList)
	if err != nil {
		return nil, err
	}
	return jobList.Jobs, nil
}

// GetJob returns the full job details of the job with the given id.
func (c *Client) GetJob(id string) (*JobDetail, error) {
	jobList := &jobDetailList{}
	err := c.get([]string{"job", id}, nil, jobList)
	if err != nil {
		return nil, err
	}
	return &jobList.Jobs[0], nil
}

// CreateJob creates a new job based on the provided structure.
func (c *Client) CreateJob(job *JobDetail) (*JobSummary, error) {
	return c.importJob(job, "create")
}

// CreateOrUpdateJob takes a job detail structure which has its ID set and either updates
// an existing job with the same id or creates a new job with that id.
func (c *Client) CreateOrUpdateJob(job *JobDetail) (*JobSummary, error) {
	return c.importJob(job, "update")
}

func (c *Client) importJob(job *JobDetail, dupeOption string) (*JobSummary, error) {
	jobList := &jobDetailList{
		Jobs: []JobDetail{*job},
	}
	args := map[string]string{
		"format":     "xml",
		"dupeOption": dupeOption,
		"uuidOption": "preserve",
	}
	result := &jobImportResults{}
	err := c.postXMLBatch([]string{"jobs", "import"}, args, jobList, result)
	if err != nil {
		return nil, err
	}

	if result.Failed.Count > 0 {
		errMsg := result.Failed.Results[0].Error
		return nil, fmt.Errorf(errMsg)
	}

	if result.Succeeded.Count != 1 {
		// Should never happen, since we send nothing in the request
		// that should cause a job to be skipped.
		return nil, fmt.Errorf("job was skipped")
	}

	return result.Succeeded.Results[0].JobSummary(), nil
}

// DeleteJob deletes the job with the given id.
func (c *Client) DeleteJob(id string) error {
	return c.delete([]string{"job", id})
}

func (c JobValueChoices) MarshalXMLAttr(name xml.Name) (xml.Attr, error) {
	if len(c) > 0 {
		return xml.Attr{name, strings.Join(c, ",")}, nil
	} else {
		return xml.Attr{}, nil
	}
}

func (c *JobValueChoices) UnmarshalXMLAttr(attr xml.Attr) error {
	values := strings.Split(attr.Value, ",")
	*c = values
	return nil
}

func (a JobCommandJobRefArguments) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
	start.Attr = []xml.Attr{
		xml.Attr{xml.Name{Local: "line"}, string(a)},
	}
	e.EncodeToken(start)
	e.EncodeToken(xml.EndElement{start.Name})
	return nil
}

func (a *JobCommandJobRefArguments) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
	type jobRefArgs struct {
		Line string `xml:"line,attr"`
	}
	args := jobRefArgs{}
	d.DecodeElement(&args, &start)

	*a = JobCommandJobRefArguments(args.Line)

	return nil
}

func (c JobPluginConfig) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
	rc := map[string]string(c)
	return marshalMapToXML(&rc, e, start, "entry", "key", "value")
}

func (c *JobPluginConfig) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
	rc := (*map[string]string)(c)
	return unmarshalMapFromXML(rc, d, start, "entry", "key", "value")
}

// JobSummary produces a JobSummary instance with values populated from the import result.
// The summary object won't have its Description populated, since import results do not
// include descriptions.
func (r *jobImportResult) JobSummary() *JobSummary {
	return &JobSummary{
		ID:          r.ID,
		Name:        r.Name,
		GroupName:   r.GroupName,
		ProjectName: r.ProjectName,
	}
}
<commit_msg>gofmt<commit_after>package rundeck

import (
	"encoding/xml"
	"fmt"
	"strings"
)

// JobSummary is an abbreviated description of a job that includes only its basic
// descriptive information and identifiers.
type JobSummary struct {
	XMLName     xml.Name `xml:"job"`
	ID          string   `xml:"id,attr"`
	Name        string   `xml:"name"`
	GroupName   string   `xml:"group"`
	ProjectName string   `xml:"project"`
	Description string   `xml:"description,omitempty"`
}

type jobSummaryList struct {
	XMLName xml.Name     `xml:"jobs"`
	Jobs    []JobSummary `xml:"job"`
}

// JobDetail is a comprehensive description of a job, including its entire definition.
type JobDetail struct {
	XMLName                   xml.Name            `xml:"job"`
	ID                        string              `xml:"uuid,omitempty"`
	Name                      string              `xml:"name"`
	GroupName                 string              `xml:"group,omitempty"`
	ProjectName               string              `xml:"context>project,omitempty"`
	OptionsConfig             *JobOptions         `xml:"context>options,omitempty"`
	Description               string              `xml:"description,omitempty"`
	LogLevel                  string              `xml:"loglevel,omitempty"`
	AllowConcurrentExecutions bool                `xml:"multipleExecutions"`
	Dispatch                  *JobDispatch        `xml:"dispatch"`
	CommandSequence           *JobCommandSequence `xml:"sequence,omitempty"`
	NodeFilter                *JobNodeFilter      `xml:"nodefilters,omitempty"`
}

type jobDetailList struct {
	XMLName xml.Name    `xml:"joblist"`
	Jobs    []JobDetail `xml:"job"`
}

// JobOptions represents the set of options on a job, if any.
type JobOptions struct {
	PreserveOrder bool        `xml:"preserveOrder,attr,omitempty"`
	Options       []JobOption `xml:"option"`
}

// JobOption represents a single option on a job.
type JobOption struct {
	XMLName xml.Name `xml:"option"`

	// The name of the option, which can be used to interpolate its value
	// into job commands.
	Name string `xml:"name,attr,omitempty"`

	// The default value of the option.
	DefaultValue string `xml:"value,attr,omitempty"`

	// A sequence of predefined choices for this option. Mutually exclusive with ValueChoicesURL.
	ValueChoices JobValueChoices `xml:"values,attr"`

	// A URL from which the predefined choices for this option will be retrieved.
	// Mutually exclusive with ValueChoices
	ValueChoicesURL string `xml:"valuesUrl,attr,omitempty"`

	// If set, Rundeck will reject values that are not in the set of predefined choices.
	RequirePredefinedChoice bool `xml:"enforcedvalues,attr,omitempty"`

	// Regular expression to be used to validate the option value.
	ValidationRegex string `xml:"regex,attr,omitempty"`

	// Description of the value to be shown in the Rundeck UI.
	Description string `xml:"description,omitempty"`

	// If set, Rundeck requires a value to be set for this option.
	IsRequired bool `xml:"required,attr,omitempty"`

	// When either ValueChoices or ValueChoicesURL is set, controls whether more than one
	// choice may be selected as the value.
	AllowsMultipleValues bool `xml:"multivalued,attr,omitempty"`

	// If AllowsMultipleChoices is set, the string that will be used to delimit the multiple
	// chosen options.
	MultiValueDelimiter string `xml:"delimeter,attr,omitempty"`

	// If set, the input for this field will be obscured in the UI. Useful for passwords
	// and other secrets.
	ObscureInput bool `xml:"secure,attr,omitempty"`

	// If set, the value can be accessed from scripts.
	ValueIsExposedToScripts bool `xml:"valueExposed,attr,omitempty"`
}

// JobValueChoices is a specialization of []string representing a sequence of predefined values
// for a job option.
type JobValueChoices []string

// JobCommandSequence describes the sequence of operations that a job will perform.
type JobCommandSequence struct {
	XMLName xml.Name `xml:"sequence"`

	// If set, Rundeck will continue with subsequent commands after a command fails.
	ContinueOnError bool `xml:"keepgoing,attr"`

	// Chooses the strategy by which Rundeck will execute commands. Can either be "node-first" or
	// "step-first".
	OrderingStrategy string `xml:"strategy,attr,omitempty"`

	// Sequence of commands to run in the sequence.
	Commands []JobCommand `xml:"command"`
}

// JobCommand describes a particular command to run within the sequence of commands on a job.
// The members of this struct are mutually-exclusive except for the pair of ScriptFile and
// ScriptFileArgs.
type JobCommand struct {
	XMLName xml.Name

	// A literal shell command to run.
	ShellCommand string `xml:"exec,omitempty"`

	// An inline program to run. This will be written to disk and executed, so if it is
	// a shell script it should have an appropriate #! line.
	Script string `xml:"script,omitempty"`

	// A pre-existing file (on the target nodes) that will be executed.
	ScriptFile string `xml:"scriptfile,omitempty"`

	// When ScriptFile is set, the arguments to provide to the script when executing it.
	ScriptFileArgs string `xml:"scriptargs,omitempty"`

	// A reference to another job to run as this command.
	Job *JobCommandJobRef `xml:"jobref"`

	// Configuration for a step plugin to run as this command.
	StepPlugin *JobPlugin `xml:"step-plugin"`

	// Configuration for a node step plugin to run as this command.
	NodeStepPlugin *JobPlugin `xml:"node-step-plugin"`
}

// JobCommandJobRef is a reference to another job that will run as one of the commands of a job.
type JobCommandJobRef struct {
	XMLName        xml.Name                  `xml:"jobref"`
	Name           string                    `xml:"name,attr"`
	GroupName      string                    `xml:"group,attr"`
	RunForEachNode bool                      `xml:"nodeStep,attr"`
	Arguments      JobCommandJobRefArguments `xml:"arg"`
}

// JobCommandJobRefArguments is a string representing the arguments in a JobCommandJobRef.
type JobCommandJobRefArguments string

// JobPlugin is a configuration for a plugin to run within a job.
type JobPlugin struct {
	XMLName xml.Name
	Type    string          `xml:"type,attr"`
	Config  JobPluginConfig `xml:"configuration"`
}

// JobPluginConfig is a specialization of map[string]string for job plugin configuration.
type JobPluginConfig map[string]string

// JobNodeFilter describes which nodes from the project's resource list will run the configured
// commands.
type JobNodeFilter struct {
	ExcludePrecedence bool   `xml:"excludeprecedence"`
	Query             string `xml:"filter,omitempty"`
}

type jobImportResults struct {
	Succeeded jobImportResultsCategory `xml:"succeeded"`
	Failed    jobImportResultsCategory `xml:"failed"`
	Skipped   jobImportResultsCategory `xml:"skipped"`
}

type jobImportResultsCategory struct {
	Count   int               `xml:"count,attr"`
	Results []jobImportResult `xml:"job"`
}

type jobImportResult struct {
	ID          string `xml:"id,omitempty"`
	Name        string `xml:"name"`
	GroupName   string `xml:"group,omitempty"`
	ProjectName string `xml:"context>project,omitempty"`
	Error       string `xml:"error"`
}

type JobDispatch struct {
	MaxThreadCount  int    `xml:"threadcount,omitempty"`
	ContinueOnError bool   `xml:"keepgoing"`
	RankAttribute   string `xml:"rankAttribute,omitempty"`
	RankOrder       string `xml:"rankOrder,omitempty"`
}

// GetJobSummariesForProject returns summaries of the jobs belonging to the named project.
func (c *Client) GetJobSummariesForProject(projectName string) ([]JobSummary, error) {
	jobList := &jobSummaryList{}
	err := c.get([]string{"project", projectName, "jobs"}, nil, jobList)
	return jobList.Jobs, err
}

// GetJobsForProject returns the full job details of the jobs belonging to the named project.
func (c *Client) GetJobsForProject(projectName string) ([]JobDetail, error) {
	jobList := &jobDetailList{}
	err := c.get([]string{"jobs", "export"}, map[string]string{"project": projectName}, jobList)
	if err != nil {
		return nil, err
	}
	return jobList.Jobs, nil
}

// GetJob returns the full job details of the job with the given id.
func (c *Client) GetJob(id string) (*JobDetail, error) {
	jobList := &jobDetailList{}
	err := c.get([]string{"job", id}, nil, jobList)
	if err != nil {
		return nil, err
	}
	return &jobList.Jobs[0], nil
}

// CreateJob creates a new job based on the provided structure.
func (c *Client) CreateJob(job *JobDetail) (*JobSummary, error) {
	return c.importJob(job, "create")
}

// CreateOrUpdateJob takes a job detail structure which has its ID set and either updates
// an existing job with the same id or creates a new job with that id.
func (c *Client) CreateOrUpdateJob(job *JobDetail) (*JobSummary, error) {
	return c.importJob(job, "update")
}

func (c *Client) importJob(job *JobDetail, dupeOption string) (*JobSummary, error) {
	jobList := &jobDetailList{
		Jobs: []JobDetail{*job},
	}
	args := map[string]string{
		"format":     "xml",
		"dupeOption": dupeOption,
		"uuidOption": "preserve",
	}
	result := &jobImportResults{}
	err := c.postXMLBatch([]string{"jobs", "import"}, args, jobList, result)
	if err != nil {
		return nil, err
	}

	if result.Failed.Count > 0 {
		errMsg := result.Failed.Results[0].Error
		return nil, fmt.Errorf(errMsg)
	}

	if result.Succeeded.Count != 1 {
		// Should never happen, since we send nothing in the request
		// that should cause a job to be skipped.
		return nil, fmt.Errorf("job was skipped")
	}

	return result.Succeeded.Results[0].JobSummary(), nil
}

// DeleteJob deletes the job with the given id.
func (c *Client) DeleteJob(id string) error {
	return c.delete([]string{"job", id})
}

func (c JobValueChoices) MarshalXMLAttr(name xml.Name) (xml.Attr, error) {
	if len(c) > 0 {
		return xml.Attr{name, strings.Join(c, ",")}, nil
	} else {
		return xml.Attr{}, nil
	}
}

func (c *JobValueChoices) UnmarshalXMLAttr(attr xml.Attr) error {
	values := strings.Split(attr.Value, ",")
	*c = values
	return nil
}

func (a JobCommandJobRefArguments) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
	start.Attr = []xml.Attr{
		xml.Attr{xml.Name{Local: "line"}, string(a)},
	}
	e.EncodeToken(start)
	e.EncodeToken(xml.EndElement{start.Name})
	return nil
}

func (a *JobCommandJobRefArguments) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
	type jobRefArgs struct {
		Line string `xml:"line,attr"`
	}
	args := jobRefArgs{}
	d.DecodeElement(&args, &start)

	*a = JobCommandJobRefArguments(args.Line)

	return nil
}

func (c JobPluginConfig) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
	rc := map[string]string(c)
	return marshalMapToXML(&rc, e, start, "entry", "key", "value")
}

func (c *JobPluginConfig) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
	rc := (*map[string]string)(c)
	return unmarshalMapFromXML(rc, d, start, "entry", "key", "value")
}

// JobSummary produces a JobSummary instance with values populated from the import result.
// The summary object won't have its Description populated, since import results do not
// include descriptions.
func (r *jobImportResult) JobSummary() *JobSummary {
	return &JobSummary{
		ID:          r.ID,
		Name:        r.Name,
		GroupName:   r.GroupName,
		ProjectName: r.ProjectName,
	}
}
<|endoftext|>


<commit_before>package blobs_test

import (
	"fmt"
	"github.com/xercoy/blobs"
	"io/ioutil"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"testing"
)

var (
	tempDir = os.TempDir()
)

func TestCreateRandomAmount(t *testing.T) {
	log.Println("\n\nStarting TestCreateRandomAmount...")

	contentSrc := strings.NewReader("icecream")

	testRunner := blobs.NewRunner(contentSrc, tempDir, "1MB", "%d.dat", 15, true)

	err := blobs.Mk(testRunner)
	if err != nil {
		t.Error(err.Error())
	}
}

func TestNewRunner(t *testing.T) {
	log.Println("\n\nStarting TestNewRunner...")

	testReader := strings.NewReader("foobarbaz")
	testRunner := blobs.NewRunner(testReader, tempDir, "2MB", "%d.dat", 3, false)

	fmtString := "Given Runner field %s not equal to the given test value."
	var value string

	//Read from the Src field
	rdrContent, err := ioutil.ReadAll(testRunner.Src)
	if err != nil {
		t.Error(err.Error())
	}

	if testRunner.Amount != 3 {
		value = "Amount"
		t.Errorf(fmtString, value)

	} else if testRunner.Unit != "2MB" {
		value = "Unit"
		t.Errorf(fmtString, value)

	} else if testRunner.Dest != tempDir {
		value = "Dest"
		t.Errorf(fmtString, value)

	} else if testRunner.FormatStr != "%d.dat" {
		value = "FormatStr"
		t.Errorf(fmtString, value)

	} else if (string)(rdrContent) != "foobarbaz" {
		value = "Src"
		t.Errorf(fmtString, value)
	}
}

func TestMk(t *testing.T) {
	log.Println("\n\nStarting TestMk...")

	testReader := strings.NewReader("helloWorld")
	testRunner := blobs.NewRunner(testReader, tempDir, "2MB", "%d.dat", 5, false)

	err := blobs.Mk(testRunner)
	if err != nil {
		t.Error(err.Error())
	}

	// Doesn't work for some reason:
	// wildcardStr := strings.Replace(testRunner.FormatStr, "%d", "*", -1)
	// Iterate through and ls each file for now...
	for i := 1; i <= testRunner.Amount; i++ {
		fileName := fmt.Sprintf(testRunner.FormatStr, i)

		blobFilter := filepath.Join(os.TempDir(), fileName)

		cmd := exec.Command("ls", "-l", blobFilter)
		cmdOutput, err := cmd.CombinedOutput()
		if err != nil {
			t.Error(err.Error())
		}

		log.Printf("%s", cmdOutput)
	}
}
<commit_msg>Modidied runner tests to use blobs directly.<commit_after>package blobs

import (
	"fmt"

	//	"github.com/xercoy/blobs"
	"io/ioutil"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"testing"
)

var (
	tempDir = os.TempDir()
)

func TestCreateRandomAmount(t *testing.T) {
	log.Println("\n\nStarting TestCreateRandomAmount...")

	contentSrc := strings.NewReader("icecream")

	testRunner := NewRunner(contentSrc, tempDir, "1MB", "%d.dat", 15, true)

	err := Mk(testRunner)
	if err != nil {
		t.Error(err.Error())
	}
}

func TestNewRunner(t *testing.T) {
	log.Println("\n\nStarting TestNewRunner...")

	testReader := strings.NewReader("foobarbaz")
	testRunner := NewRunner(testReader, tempDir, "2MB", "%d.dat", 3, false)

	fmtString := "Given Runner field %s not equal to the given test value."
	var value string

	//Read from the Src field
	rdrContent, err := ioutil.ReadAll(testRunner.Src)
	if err != nil {
		t.Error(err.Error())
	}

	if testRunner.Amount != 3 {
		value = "Amount"
		t.Errorf(fmtString, value)

	} else if testRunner.Unit != "2MB" {
		value = "Unit"
		t.Errorf(fmtString, value)

	} else if testRunner.Dest != tempDir {
		value = "Dest"
		t.Errorf(fmtString, value)

	} else if testRunner.FormatStr != "%d.dat" {
		value = "FormatStr"
		t.Errorf(fmtString, value)

	} else if (string)(rdrContent) != "foobarbaz" {
		value = "Src"
		t.Errorf(fmtString, value)
	}
}

func TestMk(t *testing.T) {
	log.Println("\n\nStarting TestMk...")

	testReader := strings.NewReader("helloWorld")
	testRunner := NewRunner(testReader, tempDir, "2MB", "%d.dat", 5, false)

	err := Mk(testRunner)
	if err != nil {
		t.Error(err.Error())
	}

	// Doesn't work for some reason:
	// wildcardStr := strings.Replace(testRunner.FormatStr, "%d", "*", -1)
	// Iterate through and ls each file for now...
	for i := 1; i <= testRunner.Amount; i++ {
		fileName := fmt.Sprintf(testRunner.FormatStr, i)

		blobFilter := filepath.Join(os.TempDir(), fileName)

		cmd := exec.Command("ls", "-l", blobFilter)
		cmdOutput, err := cmd.CombinedOutput()
		if err != nil {
			t.Error(err.Error())
		}

		log.Printf("%s", cmdOutput)
	}
}
<|endoftext|>


<commit_before>// Copyright 2021 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package msc2946 'Spaces Summary' implements https://github.com/matrix-org/matrix-doc/pull/2946
package msc2946

import (
	"context"
	"fmt"
	"net/http"
	"sync"

	"github.com/gorilla/mux"
	chttputil "github.com/matrix-org/dendrite/clientapi/httputil"
	"github.com/matrix-org/dendrite/internal/hooks"
	"github.com/matrix-org/dendrite/internal/httputil"
	roomserver "github.com/matrix-org/dendrite/roomserver/api"
	"github.com/matrix-org/dendrite/setup"
	userapi "github.com/matrix-org/dendrite/userapi/api"
	"github.com/matrix-org/gomatrixserverlib"
	"github.com/matrix-org/util"
	"github.com/tidwall/gjson"
)

const (
	ConstCreateEventContentKey = "org.matrix.msc1772.type"
	ConstSpaceChildEventType   = "org.matrix.msc1772.space.child"
	ConstSpaceParentEventType  = "org.matrix.msc1772.room.parent"
)

// SpacesRequest is the request body to POST /_matrix/client/r0/rooms/{roomID}/spaces
type SpacesRequest struct {
	MaxRoomsPerSpace int    `json:"max_rooms_per_space"`
	Limit            int    `json:"limit"`
	Batch            string `json:"batch"`
}

// Defaults sets the request defaults
func (r *SpacesRequest) Defaults() {
	r.Limit = 100
	r.MaxRoomsPerSpace = -1
}

// SpacesResponse is the response body to POST /_matrix/client/r0/rooms/{roomID}/spaces
type SpacesResponse struct {
	NextBatch string `json:"next_batch"`
	// Rooms are nodes on the space graph.
	Rooms []Room `json:"rooms"`
	// Events are edges on the space graph, exclusively m.space.child or m.room.parent events
	Events []gomatrixserverlib.ClientEvent `json:"events"`
}

// Room is a node on the space graph
type Room struct {
	gomatrixserverlib.PublicRoom
	NumRefs  int    `json:"num_refs"`
	RoomType string `json:"room_type"`
}

// Enable this MSC
func Enable(
	base *setup.BaseDendrite, rsAPI roomserver.RoomserverInternalAPI, userAPI userapi.UserInternalAPI,
) error {
	db, err := NewDatabase(&base.Cfg.MSCs.Database)
	if err != nil {
		return fmt.Errorf("Cannot enable MSC2946: %w", err)
	}
	hooks.Enable()
	hooks.Attach(hooks.KindNewEventPersisted, func(headeredEvent interface{}) {
		he := headeredEvent.(*gomatrixserverlib.HeaderedEvent)
		hookErr := db.StoreReference(context.Background(), he)
		if hookErr != nil {
			util.GetLogger(context.Background()).WithError(hookErr).WithField("event_id", he.EventID()).Error(
				"failed to StoreReference",
			)
		}
	})

	base.PublicClientAPIMux.Handle("/unstable/rooms/{roomID}/spaces",
		httputil.MakeAuthAPI("spaces", userAPI, spacesHandler(db, rsAPI)),
	).Methods(http.MethodPost, http.MethodOptions)
	return nil
}

func spacesHandler(db Database, rsAPI roomserver.RoomserverInternalAPI) func(*http.Request, *userapi.Device) util.JSONResponse {
	inMemoryBatchCache := make(map[string]set)
	return func(req *http.Request, device *userapi.Device) util.JSONResponse {
		// Extract the room ID from the request. Sanity check request data.
		params, err := httputil.URLDecodeMapValues(mux.Vars(req))
		if err != nil {
			return util.ErrorResponse(err)
		}
		roomID := params["roomID"]
		var r SpacesRequest
		r.Defaults()
		if resErr := chttputil.UnmarshalJSONRequest(req, &r); resErr != nil {
			return *resErr
		}
		if r.Limit > 100 {
			r.Limit = 100
		}
		w := walker{
			req:        &r,
			rootRoomID: roomID,
			caller:     device,
			ctx:        req.Context(),

			db:                 db,
			rsAPI:              rsAPI,
			inMemoryBatchCache: inMemoryBatchCache,
		}
		res := w.walk()
		return util.JSONResponse{
			Code: 200,
			JSON: res,
		}
	}
}

type walker struct {
	req        *SpacesRequest
	rootRoomID string
	caller     *userapi.Device
	db         Database
	rsAPI      roomserver.RoomserverInternalAPI
	ctx        context.Context

	// user ID|device ID|batch_num => event/room IDs sent to client
	inMemoryBatchCache map[string]set
	mu                 sync.Mutex
}

func (w *walker) alreadySent(id string) bool {
	w.mu.Lock()
	defer w.mu.Unlock()
	m, ok := w.inMemoryBatchCache[w.caller.UserID+"|"+w.caller.ID]
	if !ok {
		return false
	}
	return m[id]
}

func (w *walker) markSent(id string) {
	w.mu.Lock()
	defer w.mu.Unlock()
	m := w.inMemoryBatchCache[w.caller.UserID+"|"+w.caller.ID]
	if m == nil {
		m = make(set)
	}
	m[id] = true
	w.inMemoryBatchCache[w.caller.UserID+"|"+w.caller.ID] = m
}

// nolint:gocyclo
func (w *walker) walk() *SpacesResponse {
	var res SpacesResponse
	// Begin walking the graph starting with the room ID in the request in a queue of unvisited rooms
	unvisited := []string{w.rootRoomID}
	processed := make(set)
	for len(unvisited) > 0 {
		roomID := unvisited[0]
		unvisited = unvisited[1:]
		// If this room has already been processed, skip. NB: do not remember this between calls
		if processed[roomID] || roomID == "" {
			continue
		}
		// Mark this room as processed.
		processed[roomID] = true
		// Is the caller currently joined to the room or is the room `world_readable`
		// If no, skip this room. If yes, continue.
		if !w.authorised(roomID) {
			continue
		}
		// Get all `m.space.child` and `m.room.parent` state events for the room. *In addition*, get
		// all `m.space.child` and `m.room.parent` state events which *point to* (via `state_key` or `content.room_id`)
		// this room. This requires servers to store reverse lookups.
		refs, err := w.references(roomID)
		if err != nil {
			util.GetLogger(w.ctx).WithError(err).WithField("room_id", roomID).Error("failed to extract references for room")
			continue
		}

		// If this room has not ever been in `rooms` (across multiple requests), extract the
		// `PublicRoomsChunk` for this room.
		if !w.alreadySent(roomID) {
			pubRoom := w.publicRoomsChunk(roomID)
			roomType := ""
			create := w.stateEvent(roomID, "m.room.create", "")
			if create != nil {
				roomType = gjson.GetBytes(create.Content(), ConstCreateEventContentKey).Str
			}

			// Add the total number of events to `PublicRoomsChunk` under `num_refs`. Add `PublicRoomsChunk` to `rooms`.
			res.Rooms = append(res.Rooms, Room{
				PublicRoom: *pubRoom,
				NumRefs:    refs.len(),
				RoomType:   roomType,
			})
		}

		uniqueRooms := make(set)

		// If this is the root room from the original request, insert all these events into `events` if
		// they haven't been added before (across multiple requests).
		if w.rootRoomID == roomID {
			for _, ev := range refs.events() {
				if !w.alreadySent(ev.EventID()) {
					res.Events = append(res.Events, gomatrixserverlib.HeaderedToClientEvent(
						ev, gomatrixserverlib.FormatAll,
					))
					uniqueRooms[ev.RoomID()] = true
					uniqueRooms[SpaceTarget(ev)] = true
					w.markSent(ev.EventID())
				}
			}
		} else {
			// Else add them to `events` honouring the `limit` and `max_rooms_per_space` values. If either
			// are exceeded, stop adding events. If the event has already been added, do not add it again.
			numAdded := 0
			for _, ev := range refs.events() {
				if w.req.Limit > 0 && len(res.Events) >= w.req.Limit {
					break
				}
				if w.req.MaxRoomsPerSpace > 0 && numAdded >= w.req.MaxRoomsPerSpace {
					break
				}
				if w.alreadySent(ev.EventID()) {
					continue
				}
				res.Events = append(res.Events, gomatrixserverlib.HeaderedToClientEvent(
					ev, gomatrixserverlib.FormatAll,
				))
				uniqueRooms[ev.RoomID()] = true
				uniqueRooms[SpaceTarget(ev)] = true
				w.markSent(ev.EventID())
				// we don't distinguish between child state events and parent state events for the purposes of
				// max_rooms_per_space, maybe we should?
				numAdded++
			}
		}

		// For each referenced room ID in the events being returned to the caller (both parent and child)
		// add the room ID to the queue of unvisited rooms. Loop from the beginning.
		for roomID := range uniqueRooms {
			unvisited = append(unvisited, roomID)
		}
	}
	return &res
}

func (w *walker) stateEvent(roomID, evType, stateKey string) *gomatrixserverlib.HeaderedEvent {
	var queryRes roomserver.QueryCurrentStateResponse
	tuple := gomatrixserverlib.StateKeyTuple{
		EventType: evType,
		StateKey:  stateKey,
	}
	err := w.rsAPI.QueryCurrentState(w.ctx, &roomserver.QueryCurrentStateRequest{
		RoomID:      roomID,
		StateTuples: []gomatrixserverlib.StateKeyTuple{tuple},
	}, &queryRes)
	if err != nil {
		return nil
	}
	return queryRes.StateEvents[tuple]
}

func (w *walker) publicRoomsChunk(roomID string) *gomatrixserverlib.PublicRoom {
	pubRooms, err := roomserver.PopulatePublicRooms(w.ctx, []string{roomID}, w.rsAPI)
	if err != nil {
		util.GetLogger(w.ctx).WithError(err).Error("failed to PopulatePublicRooms")
		return nil
	}
	if len(pubRooms) == 0 {
		return nil
	}
	return &pubRooms[0]
}

// authorised returns true iff the user is joined this room or the room is world_readable
func (w *walker) authorised(roomID string) bool {
	hisVisTuple := gomatrixserverlib.StateKeyTuple{
		EventType: gomatrixserverlib.MRoomHistoryVisibility,
		StateKey:  "",
	}
	roomMemberTuple := gomatrixserverlib.StateKeyTuple{
		EventType: gomatrixserverlib.MRoomMember,
		StateKey:  w.caller.UserID,
	}
	var queryRes roomserver.QueryCurrentStateResponse
	err := w.rsAPI.QueryCurrentState(w.ctx, &roomserver.QueryCurrentStateRequest{
		RoomID: roomID,
		StateTuples: []gomatrixserverlib.StateKeyTuple{
			hisVisTuple, roomMemberTuple,
		},
	}, &queryRes)
	if err != nil {
		util.GetLogger(w.ctx).WithError(err).Error("failed to QueryCurrentState")
		return false
	}
	memberEv := queryRes.StateEvents[roomMemberTuple]
	hisVisEv := queryRes.StateEvents[hisVisTuple]
	if memberEv != nil {
		membership, _ := memberEv.Membership()
		if membership == gomatrixserverlib.Join {
			return true
		}
	}
	if hisVisEv != nil {
		hisVis, _ := hisVisEv.HistoryVisibility()
		if hisVis == "world_readable" {
			return true
		}
	}
	return false
}

// references returns all references pointing to or from this room.
func (w *walker) references(roomID string) (eventLookup, error) {
	events, err := w.db.References(w.ctx, roomID)
	if err != nil {
		return nil, err
	}
	el := make(eventLookup)
	for _, ev := range events {
		el.set(ev)
	}
	return el, nil
}

// state event lookup across multiple rooms keyed on event type
// NOT THREAD SAFE
type eventLookup map[string][]*gomatrixserverlib.HeaderedEvent

func (el eventLookup) set(ev *gomatrixserverlib.HeaderedEvent) {
	evs := el[ev.Type()]
	if evs == nil {
		evs = make([]*gomatrixserverlib.HeaderedEvent, 0)
	}
	evs = append(evs, ev)
	el[ev.Type()] = evs
}

func (el eventLookup) len() int {
	sum := 0
	for _, evs := range el {
		sum += len(evs)
	}
	return sum
}

func (el eventLookup) events() (events []*gomatrixserverlib.HeaderedEvent) {
	for _, evs := range el {
		events = append(events, evs...)
	}
	return
}

type set map[string]bool
<commit_msg>Per request cache for now as we don't do batching correclty<commit_after>// Copyright 2021 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package msc2946 'Spaces Summary' implements https://github.com/matrix-org/matrix-doc/pull/2946
package msc2946

import (
	"context"
	"fmt"
	"net/http"
	"sync"

	"github.com/gorilla/mux"
	chttputil "github.com/matrix-org/dendrite/clientapi/httputil"
	"github.com/matrix-org/dendrite/internal/hooks"
	"github.com/matrix-org/dendrite/internal/httputil"
	roomserver "github.com/matrix-org/dendrite/roomserver/api"
	"github.com/matrix-org/dendrite/setup"
	userapi "github.com/matrix-org/dendrite/userapi/api"
	"github.com/matrix-org/gomatrixserverlib"
	"github.com/matrix-org/util"
	"github.com/tidwall/gjson"
)

const (
	ConstCreateEventContentKey = "org.matrix.msc1772.type"
	ConstSpaceChildEventType   = "org.matrix.msc1772.space.child"
	ConstSpaceParentEventType  = "org.matrix.msc1772.room.parent"
)

// SpacesRequest is the request body to POST /_matrix/client/r0/rooms/{roomID}/spaces
type SpacesRequest struct {
	MaxRoomsPerSpace int    `json:"max_rooms_per_space"`
	Limit            int    `json:"limit"`
	Batch            string `json:"batch"`
}

// Defaults sets the request defaults
func (r *SpacesRequest) Defaults() {
	r.Limit = 100
	r.MaxRoomsPerSpace = -1
}

// SpacesResponse is the response body to POST /_matrix/client/r0/rooms/{roomID}/spaces
type SpacesResponse struct {
	NextBatch string `json:"next_batch"`
	// Rooms are nodes on the space graph.
	Rooms []Room `json:"rooms"`
	// Events are edges on the space graph, exclusively m.space.child or m.room.parent events
	Events []gomatrixserverlib.ClientEvent `json:"events"`
}

// Room is a node on the space graph
type Room struct {
	gomatrixserverlib.PublicRoom
	NumRefs  int    `json:"num_refs"`
	RoomType string `json:"room_type"`
}

// Enable this MSC
func Enable(
	base *setup.BaseDendrite, rsAPI roomserver.RoomserverInternalAPI, userAPI userapi.UserInternalAPI,
) error {
	db, err := NewDatabase(&base.Cfg.MSCs.Database)
	if err != nil {
		return fmt.Errorf("Cannot enable MSC2946: %w", err)
	}
	hooks.Enable()
	hooks.Attach(hooks.KindNewEventPersisted, func(headeredEvent interface{}) {
		he := headeredEvent.(*gomatrixserverlib.HeaderedEvent)
		hookErr := db.StoreReference(context.Background(), he)
		if hookErr != nil {
			util.GetLogger(context.Background()).WithError(hookErr).WithField("event_id", he.EventID()).Error(
				"failed to StoreReference",
			)
		}
	})

	base.PublicClientAPIMux.Handle("/unstable/rooms/{roomID}/spaces",
		httputil.MakeAuthAPI("spaces", userAPI, spacesHandler(db, rsAPI)),
	).Methods(http.MethodPost, http.MethodOptions)
	return nil
}

func spacesHandler(db Database, rsAPI roomserver.RoomserverInternalAPI) func(*http.Request, *userapi.Device) util.JSONResponse {
	return func(req *http.Request, device *userapi.Device) util.JSONResponse {
		inMemoryBatchCache := make(map[string]set)
		// Extract the room ID from the request. Sanity check request data.
		params, err := httputil.URLDecodeMapValues(mux.Vars(req))
		if err != nil {
			return util.ErrorResponse(err)
		}
		roomID := params["roomID"]
		var r SpacesRequest
		r.Defaults()
		if resErr := chttputil.UnmarshalJSONRequest(req, &r); resErr != nil {
			return *resErr
		}
		if r.Limit > 100 {
			r.Limit = 100
		}
		w := walker{
			req:        &r,
			rootRoomID: roomID,
			caller:     device,
			ctx:        req.Context(),

			db:                 db,
			rsAPI:              rsAPI,
			inMemoryBatchCache: inMemoryBatchCache,
		}
		res := w.walk()
		return util.JSONResponse{
			Code: 200,
			JSON: res,
		}
	}
}

type walker struct {
	req        *SpacesRequest
	rootRoomID string
	caller     *userapi.Device
	db         Database
	rsAPI      roomserver.RoomserverInternalAPI
	ctx        context.Context

	// user ID|device ID|batch_num => event/room IDs sent to client
	inMemoryBatchCache map[string]set
	mu                 sync.Mutex
}

func (w *walker) alreadySent(id string) bool {
	w.mu.Lock()
	defer w.mu.Unlock()
	m, ok := w.inMemoryBatchCache[w.caller.UserID+"|"+w.caller.ID]
	if !ok {
		return false
	}
	return m[id]
}

func (w *walker) markSent(id string) {
	w.mu.Lock()
	defer w.mu.Unlock()
	m := w.inMemoryBatchCache[w.caller.UserID+"|"+w.caller.ID]
	if m == nil {
		m = make(set)
	}
	m[id] = true
	w.inMemoryBatchCache[w.caller.UserID+"|"+w.caller.ID] = m
}

// nolint:gocyclo
func (w *walker) walk() *SpacesResponse {
	var res SpacesResponse
	// Begin walking the graph starting with the room ID in the request in a queue of unvisited rooms
	unvisited := []string{w.rootRoomID}
	processed := make(set)
	for len(unvisited) > 0 {
		roomID := unvisited[0]
		unvisited = unvisited[1:]
		// If this room has already been processed, skip. NB: do not remember this between calls
		if processed[roomID] || roomID == "" {
			continue
		}
		// Mark this room as processed.
		processed[roomID] = true
		// Is the caller currently joined to the room or is the room `world_readable`
		// If no, skip this room. If yes, continue.
		if !w.authorised(roomID) {
			continue
		}
		// Get all `m.space.child` and `m.room.parent` state events for the room. *In addition*, get
		// all `m.space.child` and `m.room.parent` state events which *point to* (via `state_key` or `content.room_id`)
		// this room. This requires servers to store reverse lookups.
		refs, err := w.references(roomID)
		if err != nil {
			util.GetLogger(w.ctx).WithError(err).WithField("room_id", roomID).Error("failed to extract references for room")
			continue
		}

		// If this room has not ever been in `rooms` (across multiple requests), extract the
		// `PublicRoomsChunk` for this room.
		if !w.alreadySent(roomID) {
			pubRoom := w.publicRoomsChunk(roomID)
			roomType := ""
			create := w.stateEvent(roomID, "m.room.create", "")
			if create != nil {
				roomType = gjson.GetBytes(create.Content(), ConstCreateEventContentKey).Str
			}

			// Add the total number of events to `PublicRoomsChunk` under `num_refs`. Add `PublicRoomsChunk` to `rooms`.
			res.Rooms = append(res.Rooms, Room{
				PublicRoom: *pubRoom,
				NumRefs:    refs.len(),
				RoomType:   roomType,
			})
		}

		uniqueRooms := make(set)

		// If this is the root room from the original request, insert all these events into `events` if
		// they haven't been added before (across multiple requests).
		if w.rootRoomID == roomID {
			for _, ev := range refs.events() {
				if !w.alreadySent(ev.EventID()) {
					res.Events = append(res.Events, gomatrixserverlib.HeaderedToClientEvent(
						ev, gomatrixserverlib.FormatAll,
					))
					uniqueRooms[ev.RoomID()] = true
					uniqueRooms[SpaceTarget(ev)] = true
					w.markSent(ev.EventID())
				}
			}
		} else {
			// Else add them to `events` honouring the `limit` and `max_rooms_per_space` values. If either
			// are exceeded, stop adding events. If the event has already been added, do not add it again.
			numAdded := 0
			for _, ev := range refs.events() {
				if w.req.Limit > 0 && len(res.Events) >= w.req.Limit {
					break
				}
				if w.req.MaxRoomsPerSpace > 0 && numAdded >= w.req.MaxRoomsPerSpace {
					break
				}
				if w.alreadySent(ev.EventID()) {
					continue
				}
				res.Events = append(res.Events, gomatrixserverlib.HeaderedToClientEvent(
					ev, gomatrixserverlib.FormatAll,
				))
				uniqueRooms[ev.RoomID()] = true
				uniqueRooms[SpaceTarget(ev)] = true
				w.markSent(ev.EventID())
				// we don't distinguish between child state events and parent state events for the purposes of
				// max_rooms_per_space, maybe we should?
				numAdded++
			}
		}

		// For each referenced room ID in the events being returned to the caller (both parent and child)
		// add the room ID to the queue of unvisited rooms. Loop from the beginning.
		for roomID := range uniqueRooms {
			unvisited = append(unvisited, roomID)
		}
	}
	return &res
}

func (w *walker) stateEvent(roomID, evType, stateKey string) *gomatrixserverlib.HeaderedEvent {
	var queryRes roomserver.QueryCurrentStateResponse
	tuple := gomatrixserverlib.StateKeyTuple{
		EventType: evType,
		StateKey:  stateKey,
	}
	err := w.rsAPI.QueryCurrentState(w.ctx, &roomserver.QueryCurrentStateRequest{
		RoomID:      roomID,
		StateTuples: []gomatrixserverlib.StateKeyTuple{tuple},
	}, &queryRes)
	if err != nil {
		return nil
	}
	return queryRes.StateEvents[tuple]
}

func (w *walker) publicRoomsChunk(roomID string) *gomatrixserverlib.PublicRoom {
	pubRooms, err := roomserver.PopulatePublicRooms(w.ctx, []string{roomID}, w.rsAPI)
	if err != nil {
		util.GetLogger(w.ctx).WithError(err).Error("failed to PopulatePublicRooms")
		return nil
	}
	if len(pubRooms) == 0 {
		return nil
	}
	return &pubRooms[0]
}

// authorised returns true iff the user is joined this room or the room is world_readable
func (w *walker) authorised(roomID string) bool {
	hisVisTuple := gomatrixserverlib.StateKeyTuple{
		EventType: gomatrixserverlib.MRoomHistoryVisibility,
		StateKey:  "",
	}
	roomMemberTuple := gomatrixserverlib.StateKeyTuple{
		EventType: gomatrixserverlib.MRoomMember,
		StateKey:  w.caller.UserID,
	}
	var queryRes roomserver.QueryCurrentStateResponse
	err := w.rsAPI.QueryCurrentState(w.ctx, &roomserver.QueryCurrentStateRequest{
		RoomID: roomID,
		StateTuples: []gomatrixserverlib.StateKeyTuple{
			hisVisTuple, roomMemberTuple,
		},
	}, &queryRes)
	if err != nil {
		util.GetLogger(w.ctx).WithError(err).Error("failed to QueryCurrentState")
		return false
	}
	memberEv := queryRes.StateEvents[roomMemberTuple]
	hisVisEv := queryRes.StateEvents[hisVisTuple]
	if memberEv != nil {
		membership, _ := memberEv.Membership()
		if membership == gomatrixserverlib.Join {
			return true
		}
	}
	if hisVisEv != nil {
		hisVis, _ := hisVisEv.HistoryVisibility()
		if hisVis == "world_readable" {
			return true
		}
	}
	return false
}

// references returns all references pointing to or from this room.
func (w *walker) references(roomID string) (eventLookup, error) {
	events, err := w.db.References(w.ctx, roomID)
	if err != nil {
		return nil, err
	}
	el := make(eventLookup)
	for _, ev := range events {
		el.set(ev)
	}
	return el, nil
}

// state event lookup across multiple rooms keyed on event type
// NOT THREAD SAFE
type eventLookup map[string][]*gomatrixserverlib.HeaderedEvent

func (el eventLookup) set(ev *gomatrixserverlib.HeaderedEvent) {
	evs := el[ev.Type()]
	if evs == nil {
		evs = make([]*gomatrixserverlib.HeaderedEvent, 0)
	}
	evs = append(evs, ev)
	el[ev.Type()] = evs
}

func (el eventLookup) len() int {
	sum := 0
	for _, evs := range el {
		sum += len(evs)
	}
	return sum
}

func (el eventLookup) events() (events []*gomatrixserverlib.HeaderedEvent) {
	for _, evs := range el {
		events = append(events, evs...)
	}
	return
}

type set map[string]bool
<|endoftext|>


<commit_before>// This package implements a provisioner for Packer that executes a
// saltstack highstate within the remote machine
package saltmasterless

import (
	"errors"
	"fmt"
	"github.com/mitchellh/packer/builder/common"
	"github.com/mitchellh/packer/packer"
	"os"
	"path/filepath"
	"strings"
)

var Ui packer.Ui

const DefaultTempConfigDir = "/tmp/salt"

type Config struct {
	// If true, run the salt-bootstrap script
	SkipBootstrap bool   `mapstructure:"skip_bootstrap"`
	BootstrapArgs string `mapstructure:"bootstrap_args"`

	// Local path to the salt state tree
	LocalStateTree string `mapstructure:"local_state_tree"`

	// Where files will be copied before moving to the /srv/salt directory
	TempConfigDir string `mapstructure:"temp_config_dir"`
}

type Provisioner struct {
	config Config
}

func (p *Provisioner) Prepare(raws ...interface{}) error {
	md, err := common.DecodeConfig(&p.config, raws...)
	if err != nil {
		return err
	}

	if p.config.TempConfigDir == "" {
		p.config.TempConfigDir = DefaultTempConfigDir
	}

	// Accumulate any errors
	errs := common.CheckUnusedConfig(md)

	if p.config.LocalStateTree == "" {
		errs = packer.MultiErrorAppend(errs,
			errors.New("Please specify a local_state_tree"))
	}

	if errs != nil && len(errs.Errors) > 0 {
		return errs
	}

	return nil
}

func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
	var err error

	if !p.config.SkipBootstrap {
		cmd := &packer.RemoteCmd{
			Command: fmt.Sprintf("wget -O - http://bootstrap.saltstack.org | sudo sh -s %s", p.config.BootstrapArgs),
		}
		ui.Say(fmt.Sprintf("Installing Salt with command %s", cmd))
		if err = cmd.StartWithUi(comm, ui); err != nil {
			return fmt.Errorf("Unable to install Salt: %d", err)
		}
	}

	ui.Say(fmt.Sprintf("Creating remote directory: %s", p.config.TempConfigDir))
	cmd := &packer.RemoteCmd{Command: fmt.Sprintf("mkdir -p %s", p.config.TempConfigDir)}
	if err = cmd.StartWithUi(comm, ui); err != nil {
		return fmt.Errorf("Error creating remote salt state directory: %s", err)
	}

	ui.Say(fmt.Sprintf("Uploading local state tree: %s", p.config.LocalStateTree))
	if err = UploadLocalDirectory(p.config.LocalStateTree, p.config.TempConfigDir, comm); err != nil {
		return fmt.Errorf("Error uploading local state tree to remote: %s", err)
	}

	ui.Say(fmt.Sprintf("Moving %s to /srv/salt", p.config.TempConfigDir))
	cmd = &packer.RemoteCmd{Command: fmt.Sprintf("sudo mv %s /srv/salt", p.config.TempConfigDir)}
	if err = cmd.StartWithUi(comm, ui); err != nil {
		return fmt.Errorf("Unable to move %s to /srv/salt: %d", p.config.TempConfigDir, err)
	}

	ui.Say("Running highstate")
	cmd = &packer.RemoteCmd{Command: "sudo salt-call --local state.highstate -l info"}
	if err = cmd.StartWithUi(comm, ui); err != nil {
		return fmt.Errorf("Error executing highstate: %s", err)
	}

	ui.Say("Removing /srv/salt")
	cmd = &packer.RemoteCmd{Command: "sudo rm -r /srv/salt"}
	if err = cmd.StartWithUi(comm, ui); err != nil {
		return fmt.Errorf("Unable to remove /srv/salt: %d", err)
	}

	return nil
}

func UploadLocalDirectory(localDir string, remoteDir string, comm packer.Communicator) (err error) {
	visitPath := func(localPath string, f os.FileInfo, err error) (err2 error) {
		localRelPath := strings.Replace(localPath, localDir, "", 1)
		remotePath := fmt.Sprintf("%s%s", remoteDir, localRelPath)
		if f.IsDir() && f.Name() == ".git" {
			return filepath.SkipDir
		}
		if f.IsDir() {
			// Make remote directory
			cmd := &packer.RemoteCmd{Command: fmt.Sprintf("mkdir -p %s", remotePath)}
			if err = cmd.StartWithUi(comm, Ui); err != nil {
				return err
			}
		} else {
			// Upload file to existing directory
			file, err := os.Open(localPath)
			if err != nil {
				return fmt.Errorf("Error opening file: %s", err)
			}
			defer file.Close()

			Ui.Say(fmt.Sprintf("Uploading file %s: %s", localPath, remotePath))
			if err = comm.Upload(remotePath, file); err != nil {
				return fmt.Errorf("Error uploading file: %s", err)
			}
		}
		return
	}

	err = filepath.Walk(localDir, visitPath)
	if err != nil {
		return fmt.Errorf("Error uploading local directory %s: %s", localDir, err)
	}

	return nil
}
<commit_msg>provisioner/salt-masterless: use Messages for minor steps<commit_after>// This package implements a provisioner for Packer that executes a
// saltstack highstate within the remote machine
package saltmasterless

import (
	"errors"
	"fmt"
	"github.com/mitchellh/packer/builder/common"
	"github.com/mitchellh/packer/packer"
	"os"
	"path/filepath"
	"strings"
)

var Ui packer.Ui

const DefaultTempConfigDir = "/tmp/salt"

type Config struct {
	// If true, run the salt-bootstrap script
	SkipBootstrap bool   `mapstructure:"skip_bootstrap"`
	BootstrapArgs string `mapstructure:"bootstrap_args"`

	// Local path to the salt state tree
	LocalStateTree string `mapstructure:"local_state_tree"`

	// Where files will be copied before moving to the /srv/salt directory
	TempConfigDir string `mapstructure:"temp_config_dir"`
}

type Provisioner struct {
	config Config
}

func (p *Provisioner) Prepare(raws ...interface{}) error {
	md, err := common.DecodeConfig(&p.config, raws...)
	if err != nil {
		return err
	}

	if p.config.TempConfigDir == "" {
		p.config.TempConfigDir = DefaultTempConfigDir
	}

	// Accumulate any errors
	errs := common.CheckUnusedConfig(md)

	if p.config.LocalStateTree == "" {
		errs = packer.MultiErrorAppend(errs,
			errors.New("Please specify a local_state_tree"))
	}

	if errs != nil && len(errs.Errors) > 0 {
		return errs
	}

	return nil
}

func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
	var err error

	ui.Say("Provisioning with Salt...")
	if !p.config.SkipBootstrap {
		cmd := &packer.RemoteCmd{
			Command: fmt.Sprintf("wget -O - http://bootstrap.saltstack.org | sudo sh -s %s", p.config.BootstrapArgs),
		}
		ui.Message(fmt.Sprintf("Installing Salt with command %s", cmd))
		if err = cmd.StartWithUi(comm, ui); err != nil {
			return fmt.Errorf("Unable to install Salt: %d", err)
		}
	}

	ui.Message(fmt.Sprintf("Creating remote directory: %s", p.config.TempConfigDir))
	cmd := &packer.RemoteCmd{Command: fmt.Sprintf("mkdir -p %s", p.config.TempConfigDir)}
	if err = cmd.StartWithUi(comm, ui); err != nil {
		return fmt.Errorf("Error creating remote salt state directory: %s", err)
	}

	ui.Message(fmt.Sprintf("Uploading local state tree: %s", p.config.LocalStateTree))
	if err = UploadLocalDirectory(p.config.LocalStateTree, p.config.TempConfigDir, comm); err != nil {
		return fmt.Errorf("Error uploading local state tree to remote: %s", err)
	}

	ui.Message(fmt.Sprintf("Moving %s to /srv/salt", p.config.TempConfigDir))
	cmd = &packer.RemoteCmd{Command: fmt.Sprintf("sudo mv %s /srv/salt", p.config.TempConfigDir)}
	if err = cmd.StartWithUi(comm, ui); err != nil {
		return fmt.Errorf("Unable to move %s to /srv/salt: %d", p.config.TempConfigDir, err)
	}

	ui.Message("Running highstate")
	cmd = &packer.RemoteCmd{Command: "sudo salt-call --local state.highstate -l info"}
	if err = cmd.StartWithUi(comm, ui); err != nil {
		return fmt.Errorf("Error executing highstate: %s", err)
	}

	ui.Message("Removing /srv/salt")
	cmd = &packer.RemoteCmd{Command: "sudo rm -r /srv/salt"}
	if err = cmd.StartWithUi(comm, ui); err != nil {
		return fmt.Errorf("Unable to remove /srv/salt: %d", err)
	}

	return nil
}

func UploadLocalDirectory(localDir string, remoteDir string, comm packer.Communicator) (err error) {
	visitPath := func(localPath string, f os.FileInfo, err error) (err2 error) {
		localRelPath := strings.Replace(localPath, localDir, "", 1)
		remotePath := fmt.Sprintf("%s%s", remoteDir, localRelPath)
		if f.IsDir() && f.Name() == ".git" {
			return filepath.SkipDir
		}
		if f.IsDir() {
			// Make remote directory
			cmd := &packer.RemoteCmd{Command: fmt.Sprintf("mkdir -p %s", remotePath)}
			if err = cmd.StartWithUi(comm, Ui); err != nil {
				return err
			}
		} else {
			// Upload file to existing directory
			file, err := os.Open(localPath)
			if err != nil {
				return fmt.Errorf("Error opening file: %s", err)
			}
			defer file.Close()

			Ui.Message(fmt.Sprintf("Uploading file %s: %s", localPath, remotePath))
			if err = comm.Upload(remotePath, file); err != nil {
				return fmt.Errorf("Error uploading file: %s", err)
			}
		}
		return
	}

	err = filepath.Walk(localDir, visitPath)
	if err != nil {
		return fmt.Errorf("Error uploading local directory %s: %s", localDir, err)
	}

	return nil
}
<|endoftext|>


<commit_before>package gitter

import (
	"github.com/oklahomer/go-sarah"
	"github.com/oklahomer/go-sarah/log"
	"github.com/oklahomer/go-sarah/retry"
	"golang.org/x/net/context"
	"time"
)

const (
	// GITTER is a dedicated BotType for gitter implementation.
	GITTER sarah.BotType = "gitter"
)

// Adapter stores REST/Streaming API clients' instances to let users interact with gitter.
type Adapter struct {
	config             *Config
	restAPIClient      *RestAPIClient
	streamingAPIClient *StreamingAPIClient
}

// NewAdapter creates and returns new Adapter instance.
func NewAdapter(config *Config) *Adapter {
	return &Adapter{
		config:             config,
		restAPIClient:      NewRestAPIClient(config.Token),
		streamingAPIClient: NewStreamingAPIClient(config.Token),
	}
}

// BotType returns gitter designated BotType.
func (adapter *Adapter) BotType() sarah.BotType {
	return GITTER
}

// Run fetches all belonging Room and connects to them.
func (adapter *Adapter) Run(ctx context.Context, enqueueInput func(sarah.Input) error, notifyErr func(error)) {
	// fetch joined rooms
	rooms, err := fetchRooms(ctx, adapter.restAPIClient, adapter.config.RetryLimit, adapter.config.RetryInterval)
	if err != nil {
		notifyErr(sarah.NewBotNonContinuableError(err.Error()))
		return
	}

	for _, room := range *rooms {
		go adapter.runEachRoom(ctx, room, enqueueInput)
	}
}

// SendMessage let Bot send message to gitter.
func (adapter *Adapter) SendMessage(ctx context.Context, output sarah.Output) {
	switch content := output.Content().(type) {
	case string:
		room, ok := output.Destination().(*Room)
		if !ok {
			log.Errorf("Destination is not instance of Room. %#v.", output.Destination())
			return
		}
		adapter.restAPIClient.PostMessage(ctx, room, content)
	default:
		log.Warnf("unexpected output %#v", output)
	}
}

func (adapter *Adapter) runEachRoom(ctx context.Context, room *Room, enqueueInput func(sarah.Input) error) {
	for {
		select {
		case <-ctx.Done():
			return
		default:
			log.Infof("connecting to room: %s", room.ID)
			conn, err := connectRoom(ctx, adapter.streamingAPIClient, room, adapter.config.RetryLimit, adapter.config.RetryInterval)
			if err != nil {
				log.Warnf("could not connect to room: %s", room.ID)
				return
			}

			connErr := receiveMessageRecursive(conn, enqueueInput)
			conn.Close()
			if connErr == nil {
				// Connection is intentionally closed by caller.
				// No more interaction follows.
				return
			}

			// TODO: Intentional connection close such as context.cancel also comes here.
			// It would be nice if we could detect such event to distinguish intentional behaviour and unintentional connection error.
			// But, the truth is, given error is just a privately defined error instance given by http package.
			// var errRequestCanceled = errors.New("net/http: request canceled")
			// For now, let error log appear and proceed to next loop, select case with ctx.Done() will eventually return.
			log.Error(connErr.Error())
		}
	}
}

func fetchRooms(ctx context.Context, fetcher RoomsFetcher, retrial uint, interval time.Duration) (*Rooms, error) {
	var rooms *Rooms
	err := retry.WithInterval(retrial, func() error {
		r, e := fetcher.Rooms(ctx)
		rooms = r
		return e
	}, interval)

	return rooms, err
}

func receiveMessageRecursive(messageReceiver MessageReceiver, enqueueInput func(sarah.Input) error) error {
	log.Infof("start receiving message")
	for {
		message, err := messageReceiver.Receive()

		if err == ErrEmptyPayload {
			// https://developer.gitter.im/docs/streaming-api
			// Parsers must be tolerant of occasional extra newline characters placed between messages.
			// These characters are sent as periodic "keep-alive" messages to tell clients and NAT firewalls
			// that the connection is still alive during low message volume periods.
			continue
		} else if malformedErr, ok := err.(*MalformedPayloadError); ok {
			log.Warnf("skipping malformed input: %s", malformedErr)
			continue
		} else if err != nil {
			// At this point, assume connection is unstable or is closed.
			// Let caller proceed to reconnect or quit.
			return err
		}

		enqueueInput(message)
	}
}

func connectRoom(ctx context.Context, connector StreamConnector, room *Room, retrial uint, interval time.Duration) (Connection, error) {
	var conn Connection
	err := retry.WithInterval(retrial, func() error {
		r, e := connector.Connect(ctx, room)
		if e != nil {
			log.Error(e)
		}
		conn = r
		return e
	}, interval)

	return conn, err
}

// NewStringResponse creates new sarah.CommandResponse instance with given string.
func NewStringResponse(responseContent string) *sarah.CommandResponse {
	return &sarah.CommandResponse{
		Content:     responseContent,
		UserContext: nil,
	}
}

// NewStringResponseWithNext creates new sarah.CommandResponse instance with given string and next function to continue
func NewStringResponseWithNext(responseContent string, next sarah.ContextualFunc) *sarah.CommandResponse {
	return &sarah.CommandResponse{
		Content:     responseContent,
		UserContext: sarah.NewUserContext(next),
	}
}
<commit_msg>Apply same interface to gitter adapter constructor<commit_after>package gitter

import (
	"github.com/oklahomer/go-sarah"
	"github.com/oklahomer/go-sarah/log"
	"github.com/oklahomer/go-sarah/retry"
	"golang.org/x/net/context"
	"time"
)

const (
	// GITTER is a dedicated BotType for gitter implementation.
	GITTER sarah.BotType = "gitter"
)

// AdapterOption defines function signature that Adapter's functional option must satisfy.
type AdapterOption func(adapter *Adapter) error

// Adapter stores REST/Streaming API clients' instances to let users interact with gitter.
type Adapter struct {
	config             *Config
	restAPIClient      *RestAPIClient
	streamingAPIClient *StreamingAPIClient
}

// NewAdapter creates and returns new Adapter instance.
func NewAdapter(config *Config, options ...AdapterOption) (*Adapter, error) {
	adapter := &Adapter{
		config:             config,
		restAPIClient:      NewRestAPIClient(config.Token),
		streamingAPIClient: NewStreamingAPIClient(config.Token),
	}

	for _, opt := range options {
		err := opt(adapter)
		if err != nil {
			return nil, err
		}
	}

	return adapter, nil
}

// BotType returns gitter designated BotType.
func (adapter *Adapter) BotType() sarah.BotType {
	return GITTER
}

// Run fetches all belonging Room and connects to them.
func (adapter *Adapter) Run(ctx context.Context, enqueueInput func(sarah.Input) error, notifyErr func(error)) {
	// fetch joined rooms
	rooms, err := fetchRooms(ctx, adapter.restAPIClient, adapter.config.RetryLimit, adapter.config.RetryInterval)
	if err != nil {
		notifyErr(sarah.NewBotNonContinuableError(err.Error()))
		return
	}

	for _, room := range *rooms {
		go adapter.runEachRoom(ctx, room, enqueueInput)
	}
}

// SendMessage let Bot send message to gitter.
func (adapter *Adapter) SendMessage(ctx context.Context, output sarah.Output) {
	switch content := output.Content().(type) {
	case string:
		room, ok := output.Destination().(*Room)
		if !ok {
			log.Errorf("Destination is not instance of Room. %#v.", output.Destination())
			return
		}
		adapter.restAPIClient.PostMessage(ctx, room, content)
	default:
		log.Warnf("unexpected output %#v", output)
	}
}

func (adapter *Adapter) runEachRoom(ctx context.Context, room *Room, enqueueInput func(sarah.Input) error) {
	for {
		select {
		case <-ctx.Done():
			return
		default:
			log.Infof("connecting to room: %s", room.ID)
			conn, err := connectRoom(ctx, adapter.streamingAPIClient, room, adapter.config.RetryLimit, adapter.config.RetryInterval)
			if err != nil {
				log.Warnf("could not connect to room: %s", room.ID)
				return
			}

			connErr := receiveMessageRecursive(conn, enqueueInput)
			conn.Close()
			if connErr == nil {
				// Connection is intentionally closed by caller.
				// No more interaction follows.
				return
			}

			// TODO: Intentional connection close such as context.cancel also comes here.
			// It would be nice if we could detect such event to distinguish intentional behaviour and unintentional connection error.
			// But, the truth is, given error is just a privately defined error instance given by http package.
			// var errRequestCanceled = errors.New("net/http: request canceled")
			// For now, let error log appear and proceed to next loop, select case with ctx.Done() will eventually return.
			log.Error(connErr.Error())
		}
	}
}

func fetchRooms(ctx context.Context, fetcher RoomsFetcher, retrial uint, interval time.Duration) (*Rooms, error) {
	var rooms *Rooms
	err := retry.WithInterval(retrial, func() error {
		r, e := fetcher.Rooms(ctx)
		rooms = r
		return e
	}, interval)

	return rooms, err
}

func receiveMessageRecursive(messageReceiver MessageReceiver, enqueueInput func(sarah.Input) error) error {
	log.Infof("start receiving message")
	for {
		message, err := messageReceiver.Receive()

		if err == ErrEmptyPayload {
			// https://developer.gitter.im/docs/streaming-api
			// Parsers must be tolerant of occasional extra newline characters placed between messages.
			// These characters are sent as periodic "keep-alive" messages to tell clients and NAT firewalls
			// that the connection is still alive during low message volume periods.
			continue
		} else if malformedErr, ok := err.(*MalformedPayloadError); ok {
			log.Warnf("skipping malformed input: %s", malformedErr)
			continue
		} else if err != nil {
			// At this point, assume connection is unstable or is closed.
			// Let caller proceed to reconnect or quit.
			return err
		}

		enqueueInput(message)
	}
}

func connectRoom(ctx context.Context, connector StreamConnector, room *Room, retrial uint, interval time.Duration) (Connection, error) {
	var conn Connection
	err := retry.WithInterval(retrial, func() error {
		r, e := connector.Connect(ctx, room)
		if e != nil {
			log.Error(e)
		}
		conn = r
		return e
	}, interval)

	return conn, err
}

// NewStringResponse creates new sarah.CommandResponse instance with given string.
func NewStringResponse(responseContent string) *sarah.CommandResponse {
	return &sarah.CommandResponse{
		Content:     responseContent,
		UserContext: nil,
	}
}

// NewStringResponseWithNext creates new sarah.CommandResponse instance with given string and next function to continue
func NewStringResponseWithNext(responseContent string, next sarah.ContextualFunc) *sarah.CommandResponse {
	return &sarah.CommandResponse{
		Content:     responseContent,
		UserContext: sarah.NewUserContext(next),
	}
}
<|endoftext|>


<commit_before>package nodes

import (
	"testing"

	"github.com/google/go-cmp/cmp"
)

// AllowUnexported option for cmp to make sure we can diff properly.
var cmpOpt = cmp.AllowUnexported(InfoboxNode{}, node{}, ListNode{}, TextNode{})

func TestNewInfoboxNode(t *testing.T) {
	tests := []struct {
		name      string
		inKind    InfoboxKind
		inContent []Node
		out       *InfoboxNode
	}{
		{
			name:   "PositiveEmpty",
			inKind: InfoboxPositive,
			out: &InfoboxNode{
				node: node{typ: NodeInfobox},
				Kind: InfoboxPositive,
				// TODO: Do we really want this to not be nil?
				Content: NewListNode(),
			},
		},
		{
			name:      "PositiveOneContent",
			inKind:    InfoboxPositive,
			inContent: []Node{NewTextNode("hello")},
			out: &InfoboxNode{
				node:    node{typ: NodeInfobox},
				Kind:    InfoboxPositive,
				Content: NewListNode(NewTextNode("hello")),
			},
		},
		{
			name:      "PositiveMultiContent",
			inKind:    InfoboxPositive,
			inContent: []Node{NewTextNode("orange"), NewTextNode("strawberry"), NewTextNode("pineapple")},
			out: &InfoboxNode{
				node:    node{typ: NodeInfobox},
				Kind:    InfoboxPositive,
				Content: NewListNode(NewTextNode("orange"), NewTextNode("strawberry"), NewTextNode("pineapple")),
			},
		},
		{
			name:   "NegativeEmpty",
			inKind: InfoboxNegative,
			out: &InfoboxNode{
				node: node{typ: NodeInfobox},
				Kind: InfoboxNegative,
				// TODO: Do we really want this to not be nil?
				Content: NewListNode(),
			},
		},
		{
			name:      "NegativeOneContent",
			inKind:    InfoboxNegative,
			inContent: []Node{NewTextNode("hello")},
			out: &InfoboxNode{
				node:    node{typ: NodeInfobox},
				Kind:    InfoboxNegative,
				Content: NewListNode(NewTextNode("hello")),
			},
		},
		{
			name:      "NegativeMultiContent",
			inKind:    InfoboxNegative,
			inContent: []Node{NewTextNode("orange"), NewTextNode("strawberry"), NewTextNode("pineapple")},
			out: &InfoboxNode{
				node:    node{typ: NodeInfobox},
				Kind:    InfoboxNegative,
				Content: NewListNode(NewTextNode("orange"), NewTextNode("strawberry"), NewTextNode("pineapple")),
			},
		},
		{
			// TODO: Should we set a default value?
			name:      "NoKind",
			inContent: []Node{NewTextNode("orange")},
			out: &InfoboxNode{
				node:    node{typ: NodeInfobox},
				Content: NewListNode(NewTextNode("orange")),
			},
		},
		{
			// TODO: Should this return an error instead?
			name:      "UnsupportedKind",
			inKind:    "this is not a valid kind of infobox",
			inContent: []Node{NewTextNode("orange")},
			out: &InfoboxNode{
				node:    node{typ: NodeInfobox},
				Kind:    "this is not a valid kind of infobox",
				Content: NewListNode(NewTextNode("orange")),
			},
		},
		{
			name:      "ListOfOneList",
			inKind:    InfoboxPositive,
			inContent: []Node{NewListNode(NewTextNode("a"), NewTextNode("b"))},
			out: &InfoboxNode{
				node:    node{typ: NodeInfobox},
				Kind:    InfoboxPositive,
				Content: NewListNode(NewListNode(NewTextNode("a"), NewTextNode("b"))),
			},
		},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			out := NewInfoboxNode(tc.inKind, tc.inContent...)
			if diff := cmp.Diff(tc.out, out, cmpOpt); diff != "" {
				t.Errorf("NewInfoboxNode(%q, %v) got diff (-want +got): %s", tc.inKind, tc.inContent, diff)
				return
			}
		})
	}
}
<commit_msg>Add test for empty InfoboxNode init.<commit_after>package nodes

import (
	"testing"

	"github.com/google/go-cmp/cmp"
)

// AllowUnexported option for cmp to make sure we can diff properly.
var cmpOpt = cmp.AllowUnexported(InfoboxNode{}, node{}, ListNode{}, TextNode{})

func TestNewInfoboxNode(t *testing.T) {
	tests := []struct {
		name      string
		inKind    InfoboxKind
		inContent []Node
		out       *InfoboxNode
	}{
		{
			name:   "PositiveEmpty",
			inKind: InfoboxPositive,
			out: &InfoboxNode{
				node: node{typ: NodeInfobox},
				Kind: InfoboxPositive,
				// TODO: Do we really want this to not be nil?
				Content: NewListNode(),
			},
		},
		{
			name:      "PositiveOneContent",
			inKind:    InfoboxPositive,
			inContent: []Node{NewTextNode("hello")},
			out: &InfoboxNode{
				node:    node{typ: NodeInfobox},
				Kind:    InfoboxPositive,
				Content: NewListNode(NewTextNode("hello")),
			},
		},
		{
			name:      "PositiveMultiContent",
			inKind:    InfoboxPositive,
			inContent: []Node{NewTextNode("orange"), NewTextNode("strawberry"), NewTextNode("pineapple")},
			out: &InfoboxNode{
				node:    node{typ: NodeInfobox},
				Kind:    InfoboxPositive,
				Content: NewListNode(NewTextNode("orange"), NewTextNode("strawberry"), NewTextNode("pineapple")),
			},
		},
		{
			name:   "NegativeEmpty",
			inKind: InfoboxNegative,
			out: &InfoboxNode{
				node: node{typ: NodeInfobox},
				Kind: InfoboxNegative,
				// TODO: Do we really want this to not be nil?
				Content: NewListNode(),
			},
		},
		{
			name:      "NegativeOneContent",
			inKind:    InfoboxNegative,
			inContent: []Node{NewTextNode("hello")},
			out: &InfoboxNode{
				node:    node{typ: NodeInfobox},
				Kind:    InfoboxNegative,
				Content: NewListNode(NewTextNode("hello")),
			},
		},
		{
			name:      "NegativeMultiContent",
			inKind:    InfoboxNegative,
			inContent: []Node{NewTextNode("orange"), NewTextNode("strawberry"), NewTextNode("pineapple")},
			out: &InfoboxNode{
				node:    node{typ: NodeInfobox},
				Kind:    InfoboxNegative,
				Content: NewListNode(NewTextNode("orange"), NewTextNode("strawberry"), NewTextNode("pineapple")),
			},
		},
		{
			// TODO: Should we set a default value?
			name:      "NoKind",
			inContent: []Node{NewTextNode("orange")},
			out: &InfoboxNode{
				node:    node{typ: NodeInfobox},
				Content: NewListNode(NewTextNode("orange")),
			},
		},
		{
			// TODO: Should this return an error instead?
			name:      "UnsupportedKind",
			inKind:    "this is not a valid kind of infobox",
			inContent: []Node{NewTextNode("orange")},
			out: &InfoboxNode{
				node:    node{typ: NodeInfobox},
				Kind:    "this is not a valid kind of infobox",
				Content: NewListNode(NewTextNode("orange")),
			},
		},
		{
			name:      "ListOfOneList",
			inKind:    InfoboxPositive,
			inContent: []Node{NewListNode(NewTextNode("a"), NewTextNode("b"))},
			out: &InfoboxNode{
				node:    node{typ: NodeInfobox},
				Kind:    InfoboxPositive,
				Content: NewListNode(NewListNode(NewTextNode("a"), NewTextNode("b"))),
			},
		},
		{
			name: "Empty",
			out: &InfoboxNode{
				node:    node{typ: NodeInfobox},
				Content: NewListNode(),
			},
		},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			out := NewInfoboxNode(tc.inKind, tc.inContent...)
			if diff := cmp.Diff(tc.out, out, cmpOpt); diff != "" {
				t.Errorf("NewInfoboxNode(%q, %v) got diff (-want +got): %s", tc.inKind, tc.inContent, diff)
				return
			}
		})
	}
}
<|endoftext|>


<commit_before>package conio

import (
	"fmt"
	"strings"
)

type Result int

const (
	CONTINUE Result = iota
	ENTER    Result = iota
	ABORT    Result = iota
)

type KeyFuncT interface {
	Call(buffer *Buffer) Result
}

type KeyGoFuncT struct {
	F func(buffer *Buffer) Result
}

func (this *KeyGoFuncT) Call(buffer *Buffer) Result {
	return this.F(buffer)
}

var keyMap = map[rune]KeyFuncT{
	name2char[K_CTRL_A]: name2func[F_BEGINNING_OF_LINE],
	name2char[K_CTRL_B]: name2func[F_BACKWARD_CHAR],
	name2char[K_CTRL_C]: name2func[F_INTR],
	name2char[K_CTRL_D]: name2func[F_DELETE_OR_ABORT],
	name2char[K_CTRL_E]: name2func[F_END_OF_LINE],
	name2char[K_CTRL_F]: name2func[F_FORARD_CHAR],
	name2char[K_CTRL_H]: name2func[F_BACKWARD_DELETE_CHAR],
	name2char[K_CTRL_K]: name2func[F_KILL_LINE],
	name2char[K_CTRL_L]: name2func[F_CLEAR_SCREEN],
	name2char[K_CTRL_M]: name2func[F_ACCEPT_LINE],
	name2char[K_CTRL_U]: name2func[F_UNIX_LINE_DISCARD],
	name2char[K_CTRL_Y]: name2func[F_YANK],
	name2char[K_DELETE]: name2func[F_DELETE_CHAR],
	name2char[K_ENTER]:  name2func[F_ACCEPT_LINE],
	name2char[K_ESCAPE]: name2func[F_KILL_WHOLE_LINE],
	name2char[K_CTRL_N]: name2func[F_HISTORY_DOWN],
	name2char[K_CTRL_P]: name2func[F_HISTORY_UP],
}

var scanMap = map[uint16]KeyFuncT{
	name2scan[K_CTRL]:   name2func[F_PASS],
	name2scan[K_DELETE]: name2func[F_DELETE_CHAR],
	name2scan[K_END]:    name2func[F_END_OF_LINE],
	name2scan[K_HOME]:   name2func[F_BEGINNING_OF_LINE],
	name2scan[K_LEFT]:   name2func[F_BACKWARD_CHAR],
	name2scan[K_RIGHT]:  name2func[F_FORARD_CHAR],
	name2scan[K_SHIFT]:  name2func[F_PASS],
	name2scan[K_DOWN]:   name2func[F_HISTORY_DOWN],
	name2scan[K_UP]:     name2func[F_HISTORY_UP],
}

var altMap = map[uint16]KeyFuncT{
	name2alt[K_ALT_V]: name2func[F_YANK],
}

func normWord(src string) string {
	return strings.Replace(strings.ToUpper(src), "-", "_", -1)
}

func BindKeyFunc(keyName string, funcValue KeyFuncT) error {
	keyName_ := normWord(keyName)
	if altValue, altOk := name2alt[keyName_]; altOk {
		altMap[altValue] = funcValue
		return nil
	} else if charValue, charOk := name2char[keyName_]; charOk {
		keyMap[charValue] = funcValue
		return nil
	} else if scanValue, scanOk := name2scan[keyName_]; scanOk {
		scanMap[scanValue] = funcValue
		return nil
	} else {
		return fmt.Errorf("%s: no such keyname", keyName)
	}
}

func GetFunc(funcName string) (KeyFuncT, error) {
	rc, ok := name2func[normWord(funcName)]
	if ok {
		return rc, nil
	} else {
		return nil, fmt.Errorf("%s: not found in the function-list", funcName)
	}
}

func BindKeySymbol(keyName, funcName string) error {
	funcValue, funcOk := name2func[normWord(funcName)]
	if !funcOk {
		return fmt.Errorf("%s: no such function.", funcName)
	}
	return BindKeyFunc(keyName, funcValue)
}

func BindKeySymbolFunc(keyName, funcName string, funcValue KeyFuncT) error {
	name2func[normWord(funcName)] = funcValue
	return BindKeyFunc(keyName, funcValue)
}

func ReadLinePromptFunc(promptFunc func() int) (string, Result) {
	this := Buffer{Buffer: make([]rune, 20)}
	this.ViewWidth, _ = GetScreenBufferInfo().ViewSize()
	this.ViewWidth--

	this.Prompt = promptFunc
	if this.Prompt != nil {
		this.ViewWidth = this.ViewWidth - this.Prompt()
	}
	for {
		stdOut.Flush()
		shineCursor()
		this.Unicode, this.Keycode, this.ShiftState = GetKey()
		var f KeyFuncT
		var ok bool
		if (this.ShiftState & ALT_PRESSED) != 0 {
			f, ok = altMap[this.Keycode]
			if !ok {
				continue
			}
		} else if this.Unicode != 0 {
			f, ok = keyMap[this.Unicode]
			if !ok {
				//f = KeyFuncInsertReport
				f = &KeyGoFuncT{KeyFuncInsertSelf}
			}
		} else {
			f, ok = scanMap[this.Keycode]
			if !ok {
				f = &KeyGoFuncT{KeyFuncPass}
			}
		}
		rc := f.Call(&this)
		if rc != CONTINUE {
			stdOut.WriteRune('\n')
			stdOut.Flush()
			result := this.String()
			if result == "" {
				HistoryResetPointer()
			}
			if last := LastHistory(); last == nil || result != last.Line {
				HistoryPush(result)
			}
			return result, rc
		}
	}
}

// Not used on NYAGOS. Provide this as library for other applications.
func ReadLinePromptStr(promptStr string) (string, Result) {
	return ReadLinePromptFunc(func() int {
		fmt.Print(promptStr)
		return len(promptStr)
	})
}
<commit_msg>Let functions bound on ALT-* not run with Ctrl<commit_after>package conio

import (
	"fmt"
	"strings"
)

type Result int

const (
	CONTINUE Result = iota
	ENTER    Result = iota
	ABORT    Result = iota
)

type KeyFuncT interface {
	Call(buffer *Buffer) Result
}

type KeyGoFuncT struct {
	F func(buffer *Buffer) Result
}

func (this *KeyGoFuncT) Call(buffer *Buffer) Result {
	return this.F(buffer)
}

var keyMap = map[rune]KeyFuncT{
	name2char[K_CTRL_A]: name2func[F_BEGINNING_OF_LINE],
	name2char[K_CTRL_B]: name2func[F_BACKWARD_CHAR],
	name2char[K_CTRL_C]: name2func[F_INTR],
	name2char[K_CTRL_D]: name2func[F_DELETE_OR_ABORT],
	name2char[K_CTRL_E]: name2func[F_END_OF_LINE],
	name2char[K_CTRL_F]: name2func[F_FORARD_CHAR],
	name2char[K_CTRL_H]: name2func[F_BACKWARD_DELETE_CHAR],
	name2char[K_CTRL_K]: name2func[F_KILL_LINE],
	name2char[K_CTRL_L]: name2func[F_CLEAR_SCREEN],
	name2char[K_CTRL_M]: name2func[F_ACCEPT_LINE],
	name2char[K_CTRL_U]: name2func[F_UNIX_LINE_DISCARD],
	name2char[K_CTRL_Y]: name2func[F_YANK],
	name2char[K_DELETE]: name2func[F_DELETE_CHAR],
	name2char[K_ENTER]:  name2func[F_ACCEPT_LINE],
	name2char[K_ESCAPE]: name2func[F_KILL_WHOLE_LINE],
	name2char[K_CTRL_N]: name2func[F_HISTORY_DOWN],
	name2char[K_CTRL_P]: name2func[F_HISTORY_UP],
}

var scanMap = map[uint16]KeyFuncT{
	name2scan[K_CTRL]:   name2func[F_PASS],
	name2scan[K_DELETE]: name2func[F_DELETE_CHAR],
	name2scan[K_END]:    name2func[F_END_OF_LINE],
	name2scan[K_HOME]:   name2func[F_BEGINNING_OF_LINE],
	name2scan[K_LEFT]:   name2func[F_BACKWARD_CHAR],
	name2scan[K_RIGHT]:  name2func[F_FORARD_CHAR],
	name2scan[K_SHIFT]:  name2func[F_PASS],
	name2scan[K_DOWN]:   name2func[F_HISTORY_DOWN],
	name2scan[K_UP]:     name2func[F_HISTORY_UP],
}

var altMap = map[uint16]KeyFuncT{
	name2alt[K_ALT_V]: name2func[F_YANK],
}

func normWord(src string) string {
	return strings.Replace(strings.ToUpper(src), "-", "_", -1)
}

func BindKeyFunc(keyName string, funcValue KeyFuncT) error {
	keyName_ := normWord(keyName)
	if altValue, altOk := name2alt[keyName_]; altOk {
		altMap[altValue] = funcValue
		return nil
	} else if charValue, charOk := name2char[keyName_]; charOk {
		keyMap[charValue] = funcValue
		return nil
	} else if scanValue, scanOk := name2scan[keyName_]; scanOk {
		scanMap[scanValue] = funcValue
		return nil
	} else {
		return fmt.Errorf("%s: no such keyname", keyName)
	}
}

func GetFunc(funcName string) (KeyFuncT, error) {
	rc, ok := name2func[normWord(funcName)]
	if ok {
		return rc, nil
	} else {
		return nil, fmt.Errorf("%s: not found in the function-list", funcName)
	}
}

func BindKeySymbol(keyName, funcName string) error {
	funcValue, funcOk := name2func[normWord(funcName)]
	if !funcOk {
		return fmt.Errorf("%s: no such function.", funcName)
	}
	return BindKeyFunc(keyName, funcValue)
}

func BindKeySymbolFunc(keyName, funcName string, funcValue KeyFuncT) error {
	name2func[normWord(funcName)] = funcValue
	return BindKeyFunc(keyName, funcValue)
}

func ReadLinePromptFunc(promptFunc func() int) (string, Result) {
	this := Buffer{Buffer: make([]rune, 20)}
	this.ViewWidth, _ = GetScreenBufferInfo().ViewSize()
	this.ViewWidth--

	this.Prompt = promptFunc
	if this.Prompt != nil {
		this.ViewWidth = this.ViewWidth - this.Prompt()
	}
	for {
		stdOut.Flush()
		shineCursor()
		this.Unicode, this.Keycode, this.ShiftState = GetKey()
		var f KeyFuncT
		var ok bool
		if (this.ShiftState&ALT_PRESSED) != 0 &&
			(this.ShiftState&CTRL_PRESSED) == 0 {
			f, ok = altMap[this.Keycode]
			if !ok {
				continue
			}
		} else if this.Unicode != 0 {
			f, ok = keyMap[this.Unicode]
			if !ok {
				//f = KeyFuncInsertReport
				f = &KeyGoFuncT{KeyFuncInsertSelf}
			}
		} else {
			f, ok = scanMap[this.Keycode]
			if !ok {
				f = &KeyGoFuncT{KeyFuncPass}
			}
		}
		rc := f.Call(&this)
		if rc != CONTINUE {
			stdOut.WriteRune('\n')
			stdOut.Flush()
			result := this.String()
			if result == "" {
				HistoryResetPointer()
			}
			if last := LastHistory(); last == nil || result != last.Line {
				HistoryPush(result)
			}
			return result, rc
		}
	}
}

// Not used on NYAGOS. Provide this as library for other applications.
func ReadLinePromptStr(promptStr string) (string, Result) {
	return ReadLinePromptFunc(func() int {
		fmt.Print(promptStr)
		return len(promptStr)
	})
}
<|endoftext|>