<commit_before>// Copyright 2020 Google LLC
//
// 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
//
//     https://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.
//
// Authors:
// - Alicja Kwiecinska (kwiecinskaa@google.com) github: alicjakwie
// - Evan Spendlove, GitHub: evanSpendlove.
//
// probe implements a probe that monitors a storage system using the
// Hermes algorithm.

// Package probe implements the probe that Hermes uses to monitor
// a storage system.
package probe

import (
	"context"
	"fmt"
	"sync"
	"time"

	"github.com/google/cloudprober/logger"
	cpmetrics "github.com/google/cloudprober/metrics"
	"github.com/google/cloudprober/probes/options"
	"github.com/googleinterns/step224-2020/hermes/probe/metrics"
	"github.com/googleinterns/step224-2020/hermes/probe/target"

	probepb "github.com/googleinterns/step224-2020/config/proto"
	journalpb "github.com/googleinterns/step224-2020/hermes/proto"
)

const (
	// TODO(evanSpendlove): Refactor to use constant from metrics.go
	probeLatency = "hermes_probe_latency_seconds"
)

// Probe holds aggregate information about all probe runs, per-target.
// It also holds the config and options used to initialise the probe.
type Probe struct {
	name    string
	config  *probepb.HermesProbeDef
	targets []*target.Target
	opts    *options.Options
	logger  *logger.Logger
}

// interval returns the probing interval in seconds as a time.Duration.
// Returns:
//	- time.Duration: returns the probing interval
func (p *Probe) interval() time.Duration {
	return time.Duration(p.config.GetIntervalSec()) * time.Second
}

// timeout returns the probe timeout in seconds as a time.Duration.
// Returns:
//	- time.Duration: returns the probe timeout
func (p *Probe) timeout() time.Duration {
	return time.Duration(p.config.GetTimeoutSec()) * time.Second
}

// Init initializes the probe with the given parameters.
// This is a required method to iplement the cloudprober.Probes.Probe interface.
func (p *Probe) Init(name string, opts *options.Options) error {
	// Cast from Cloudprober defined interface to HermesProbeDef.
	// This allows for accessing the variables and methods of a HermesProbeDef object.
	conf, ok := opts.ProbeConf.(*probepb.HermesProbeDef)
	if !ok {
		return fmt.Errorf("invalid argument: opts.ProbeConf is not of type *probepb.HermesProbeDef")
	}
	p.name = name
	p.config = conf

	for _, t := range p.config.GetTargets() {
		p.targets = append(p.targets, &target.Target{
			Target: t,
			Journal: &journalpb.StateJournal{
				Intent:    &journalpb.Intent{},
				Filenames: make(map[int32]string),
			},
		})
	}
	p.opts = opts
	p.logger = opts.Logger

	return nil
}

// Start runs the probe indefinitely, unless cancelled, at the configured interval.
// Probe metrics will be sent via the metricChan at the end of the probe run.
// This is a required method to implement the cloudprober.Probes.Probe interface.
// Arguments:
//	- ctx: context provided for cancelling probe.
//	- metricChan: bidirectional channel used for sending metrics to be surfaced.
//		- Must be bidirectional to satisfy cloudprober.Probes.Probe interface.
func (p *Probe) Start(ctx context.Context, metricChan chan *cpmetrics.EventMetrics) {
	probeTicker := time.NewTicker(p.interval())

	for {
		select {
		case <-ctx.Done():
			probeTicker.Stop()
			return
		case <-probeTicker.C:
			p.runProbe(ctx, metricChan)
		}
	}
}

// reportMetrics sends the metrics recorded in the current probe run to Cloudprober.
// Arguments:
//	- run: metrics from a probe run on a target.
//	- metricChan: metric channel passed from Cloudprober.
func reportMetrics(run *metrics.Metrics, metricChan chan<- *cpmetrics.EventMetrics) {
	for _, op := range run.ProbeOpLatency {
		for _, m := range op {
			m.Timestamp = time.Now()
			metricChan <- m
		}
	}

	for _, call := range run.APICallLatency {
		for _, m := range call {
			m.Timestamp = time.Now()
			metricChan <- m
		}
	}
}

// runProbe runs the probe against each target, collects metrics on probe run
// and surface metrics to Cloudprober.
// Arguments:
//	- ctx: pass context to allow for complete cancellation of the probe.
//	- metricChan: pass the metrics channel for surfacing metrics to Cloudprober.
func (p *Probe) runProbe(ctx context.Context, metricChan chan<- *cpmetrics.EventMetrics) {
	var wg sync.WaitGroup
	for _, t := range p.targets {
		wg.Add(1)
		t := t
		go func() {
			defer wg.Done()
			if t.LatencyMetrics == nil {
				lm, err := metrics.NewMetrics(p.config, t.Target)
				if err != nil {
					p.logger.Errorf("NewMetrics(%v) failed: %v", t.Target, err)
					return
				}
				t.LatencyMetrics = lm
			}

			probeCtx, _ := context.WithDeadline(ctx, time.Now().Add(p.interval()))
			// TODO(evanSpendlove): Refactor to use closure func from metrics.go in metrics PR.
			start := time.Now()
			status, err := p.runProbeForTarget(probeCtx, t)
			if err != nil {
				p.logger.Errorf(err.Error())
			}

			t.LatencyMetrics.ProbeOpLatency[metrics.TotalProbeRun][status].Metric(probeLatency).AddFloat64(time.Now().Sub(start).Seconds())
			reportMetrics(t.LatencyMetrics, metricChan)
		}()
	}
	wg.Wait()
}

// runProbeForTarget runs the Hermes probing algorithm on a single target.
// Arguments:
//	- ctx: pass context to allow for cancellation of the probe.
//	- target: the target to be probed
// Returns:
//	- status: returns the exit status of the probe run.
//	- error: returns an error if one occurred during the probe run.
func (p *Probe) runProbeForTarget(ctx context.Context, target *target.Target) (metrics.ExitStatus, error) {
	// TODO(evanSpendlove): Add implementation of runProbeForTarget, i.e. Hermes probing algorithm.
	return metrics.Success, nil
}
<commit_msg>fixed typo.<commit_after>// Copyright 2020 Google LLC
//
// 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
//
//     https://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.
//
// Authors:
// - Alicja Kwiecinska (kwiecinskaa@google.com) github: alicjakwie
// - Evan Spendlove, GitHub: evanSpendlove.
//
// probe implements a probe that monitors a storage system using the
// Hermes algorithm.

// Package probe implements the probe that Hermes uses to monitor
// a storage system.
package probe

import (
	"context"
	"fmt"
	"sync"
	"time"

	"github.com/google/cloudprober/logger"
	cpmetrics "github.com/google/cloudprober/metrics"
	"github.com/google/cloudprober/probes/options"
	"github.com/googleinterns/step224-2020/hermes/probe/metrics"
	"github.com/googleinterns/step224-2020/hermes/probe/target"

	probepb "github.com/googleinterns/step224-2020/config/proto"
	journalpb "github.com/googleinterns/step224-2020/hermes/proto"
)

const (
	// TODO(evanSpendlove): Refactor to use constant from metrics.go
	probeLatency = "hermes_probe_latency_seconds"
)

// Probe holds aggregate information about all probe runs, per-target.
// It also holds the config and options used to initialise the probe.
type Probe struct {
	name    string
	config  *probepb.HermesProbeDef
	targets []*target.Target
	opts    *options.Options
	logger  *logger.Logger
}

// interval returns the probing interval in seconds as a time.Duration.
// Returns:
//	- time.Duration: returns the probing interval
func (p *Probe) interval() time.Duration {
	return time.Duration(p.config.GetIntervalSec()) * time.Second
}

// timeout returns the probe timeout in seconds as a time.Duration.
// Returns:
//	- time.Duration: returns the probe timeout
func (p *Probe) timeout() time.Duration {
	return time.Duration(p.config.GetTimeoutSec()) * time.Second
}

// Init initializes the probe with the given parameters.
// This is a required method to implement the cloudprober.Probes.Probe interface.
func (p *Probe) Init(name string, opts *options.Options) error {
	// Cast from Cloudprober defined interface to HermesProbeDef.
	// This allows for accessing the variables and methods of a HermesProbeDef object.
	conf, ok := opts.ProbeConf.(*probepb.HermesProbeDef)
	if !ok {
		return fmt.Errorf("invalid argument: opts.ProbeConf is not of type *probepb.HermesProbeDef")
	}
	p.name = name
	p.config = conf

	for _, t := range p.config.GetTargets() {
		p.targets = append(p.targets, &target.Target{
			Target: t,
			Journal: &journalpb.StateJournal{
				Intent:    &journalpb.Intent{},
				Filenames: make(map[int32]string),
			},
		})
	}
	p.opts = opts
	p.logger = opts.Logger

	return nil
}

// Start runs the probe indefinitely, unless cancelled, at the configured interval.
// Probe metrics will be sent via the metricChan at the end of the probe run.
// This is a required method to implement the cloudprober.Probes.Probe interface.
// Arguments:
//	- ctx: context provided for cancelling probe.
//	- metricChan: bidirectional channel used for sending metrics to be surfaced.
//		- Must be bidirectional to satisfy cloudprober.Probes.Probe interface.
func (p *Probe) Start(ctx context.Context, metricChan chan *cpmetrics.EventMetrics) {
	probeTicker := time.NewTicker(p.interval())

	for {
		select {
		case <-ctx.Done():
			probeTicker.Stop()
			return
		case <-probeTicker.C:
			p.runProbe(ctx, metricChan)
		}
	}
}

// reportMetrics sends the metrics recorded in the current probe run to Cloudprober.
// Arguments:
//	- run: metrics from a probe run on a target.
//	- metricChan: metric channel passed from Cloudprober.
func reportMetrics(run *metrics.Metrics, metricChan chan<- *cpmetrics.EventMetrics) {
	for _, op := range run.ProbeOpLatency {
		for _, m := range op {
			m.Timestamp = time.Now()
			metricChan <- m
		}
	}

	for _, call := range run.APICallLatency {
		for _, m := range call {
			m.Timestamp = time.Now()
			metricChan <- m
		}
	}
}

// runProbe runs the probe against each target, collects metrics on probe run
// and surface metrics to Cloudprober.
// Arguments:
//	- ctx: pass context to allow for complete cancellation of the probe.
//	- metricChan: pass the metrics channel for surfacing metrics to Cloudprober.
func (p *Probe) runProbe(ctx context.Context, metricChan chan<- *cpmetrics.EventMetrics) {
	var wg sync.WaitGroup
	for _, t := range p.targets {
		wg.Add(1)
		t := t
		go func() {
			defer wg.Done()
			if t.LatencyMetrics == nil {
				lm, err := metrics.NewMetrics(p.config, t.Target)
				if err != nil {
					p.logger.Errorf("NewMetrics(%v) failed: %v", t.Target, err)
					return
				}
				t.LatencyMetrics = lm
			}

			probeCtx, _ := context.WithDeadline(ctx, time.Now().Add(p.interval()))
			// TODO(evanSpendlove): Refactor to use closure func from metrics.go in metrics PR.
			start := time.Now()
			status, err := p.runProbeForTarget(probeCtx, t)
			if err != nil {
				p.logger.Errorf(err.Error())
			}

			t.LatencyMetrics.ProbeOpLatency[metrics.TotalProbeRun][status].Metric(probeLatency).AddFloat64(time.Now().Sub(start).Seconds())
			reportMetrics(t.LatencyMetrics, metricChan)
		}()
	}
	wg.Wait()
}

// runProbeForTarget runs the Hermes probing algorithm on a single target.
// Arguments:
//	- ctx: pass context to allow for cancellation of the probe.
//	- target: the target to be probed
// Returns:
//	- status: returns the exit status of the probe run.
//	- error: returns an error if one occurred during the probe run.
func (p *Probe) runProbeForTarget(ctx context.Context, target *target.Target) (metrics.ExitStatus, error) {
	// TODO(evanSpendlove): Add implementation of runProbeForTarget, i.e. Hermes probing algorithm.
	return metrics.Success, nil
}
<|endoftext|>


<commit_before>// +build !containers_image_ostree_stub

package ostree

import (
	"bytes"
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"strings"

	"github.com/pkg/errors"

	"github.com/containers/image/directory/explicitfilepath"
	"github.com/containers/image/docker/reference"
	"github.com/containers/image/transports"
	"github.com/containers/image/types"
)

const defaultOSTreeRepo = "/ostree/repo"

// Transport is an ImageTransport for ostree paths.
var Transport = ostreeTransport{}

type ostreeTransport struct{}

func (t ostreeTransport) Name() string {
	return "ostree"
}

func init() {
	transports.Register(Transport)
}

// ValidatePolicyConfigurationScope checks that scope is a valid name for a signature.PolicyTransportScopes keys
// (i.e. a valid PolicyConfigurationIdentity() or PolicyConfigurationNamespaces() return value).
// It is acceptable to allow an invalid value which will never be matched, it can "only" cause user confusion.
// scope passed to this function will not be "", that value is always allowed.
func (t ostreeTransport) ValidatePolicyConfigurationScope(scope string) error {
	sep := strings.Index(scope, ":")
	if sep < 0 {
		return errors.Errorf("Invalid ostree: scope %s: Must include a repo", scope)
	}
	repo := scope[:sep]

	if !strings.HasPrefix(repo, "/") {
		return errors.Errorf("Invalid ostree: scope %s: repository must be an absolute path", scope)
	}
	cleaned := filepath.Clean(repo)
	if cleaned != repo {
		return errors.Errorf(`Invalid ostree: scope %s: Uses non-canonical path format, perhaps try with path %s`, scope, cleaned)
	}

	// FIXME? In the namespaces within a repo,
	// we could be verifying the various character set and length restrictions
	// from docker/distribution/reference.regexp.go, but other than that there
	// are few semantically invalid strings.
	return nil
}

// ostreeReference is an ImageReference for ostree paths.
type ostreeReference struct {
	image      string
	branchName string
	repo       string
}

func (t ostreeTransport) ParseReference(ref string) (types.ImageReference, error) {
	var repo = ""
	var image = ""
	s := strings.SplitN(ref, "@/", 2)
	if len(s) == 1 {
		image, repo = s[0], defaultOSTreeRepo
	} else {
		image, repo = s[0], "/"+s[1]
	}

	return NewReference(image, repo)
}

// NewReference returns an OSTree reference for a specified repo and image.
func NewReference(image string, repo string) (types.ImageReference, error) {
	// image is not _really_ in a containers/image/docker/reference format;
	// as far as the libOSTree ociimage/* namespace is concerned, it is more or
	// less an arbitrary string with an implied tag.
	// We use the reference.* parsers basically for the default tag name in
	// reference.TagNameOnly, and incidentally for some character set and length
	// restrictions.
	var ostreeImage reference.Named
	s := strings.SplitN(image, ":", 2)

	named, err := reference.WithName(s[0])
	if err != nil {
		return nil, err
	}

	if len(s) == 1 {
		ostreeImage = reference.TagNameOnly(named)
	} else {
		ostreeImage, err = reference.WithTag(named, s[1])
		if err != nil {
			return nil, err
		}
	}

	resolved, err := explicitfilepath.ResolvePathToFullyExplicit(repo)
	if err != nil {
		// With os.IsNotExist(err), the parent directory of repo is also not existent;
		// that should ordinarily not happen, but it would be a bit weird to reject
		// references which do not specify a repo just because the implicit defaultOSTreeRepo
		// does not exist.
		if os.IsNotExist(err) && repo == defaultOSTreeRepo {
			resolved = repo
		} else {
			return nil, err
		}
	}
	// This is necessary to prevent directory paths returned by PolicyConfigurationNamespaces
	// from being ambiguous with values of PolicyConfigurationIdentity.
	if strings.Contains(resolved, ":") {
		return nil, errors.Errorf("Invalid OSTreeCI reference %s@%s: path %s contains a colon", image, repo, resolved)
	}

	return ostreeReference{
		image:      ostreeImage.String(),
		branchName: encodeOStreeRef(ostreeImage.String()),
		repo:       resolved,
	}, nil
}

func (ref ostreeReference) Transport() types.ImageTransport {
	return Transport
}

// StringWithinTransport returns a string representation of the reference, which MUST be such that
// reference.Transport().ParseReference(reference.StringWithinTransport()) returns an equivalent reference.
// NOTE: The returned string is not promised to be equal to the original input to ParseReference;
// e.g. default attribute values omitted by the user may be filled in in the return value, or vice versa.
// WARNING: Do not use the return value in the UI to describe an image, it does not contain the Transport().Name() prefix.
func (ref ostreeReference) StringWithinTransport() string {
	return fmt.Sprintf("%s@%s", ref.image, ref.repo)
}

// DockerReference returns a Docker reference associated with this reference
// (fully explicit, i.e. !reference.IsNameOnly, but reflecting user intent,
// not e.g. after redirect or alias processing), or nil if unknown/not applicable.
func (ref ostreeReference) DockerReference() reference.Named {
	return nil
}

func (ref ostreeReference) PolicyConfigurationIdentity() string {
	return fmt.Sprintf("%s:%s", ref.repo, ref.image)
}

// PolicyConfigurationNamespaces returns a list of other policy configuration namespaces to search
// for if explicit configuration for PolicyConfigurationIdentity() is not set.  The list will be processed
// in order, terminating on first match, and an implicit "" is always checked at the end.
// It is STRONGLY recommended for the first element, if any, to be a prefix of PolicyConfigurationIdentity(),
// and each following element to be a prefix of the element preceding it.
func (ref ostreeReference) PolicyConfigurationNamespaces() []string {
	s := strings.SplitN(ref.image, ":", 2)
	if len(s) != 2 { // Coverage: Should never happen, NewReference above ensures ref.image has a :tag.
		panic(fmt.Sprintf("Internal inconsistency: ref.image value %q does not have a :tag", ref.image))
	}
	name := s[0]
	res := []string{}
	for {
		res = append(res, fmt.Sprintf("%s:%s", ref.repo, name))

		lastSlash := strings.LastIndex(name, "/")
		if lastSlash == -1 {
			break
		}
		name = name[:lastSlash]
	}
	return res
}

// NewImage returns a types.Image for this reference, possibly specialized for this ImageTransport.
// The caller must call .Close() on the returned Image.
// NOTE: If any kind of signature verification should happen, build an UnparsedImage from the value returned by NewImageSource,
// verify that UnparsedImage, and convert it into a real Image via image.FromUnparsedImage.
func (ref ostreeReference) NewImage(ctx *types.SystemContext) (types.Image, error) {
	return nil, errors.New("Reading ostree: images is currently not supported")
}

// NewImageSource returns a types.ImageSource for this reference.
// The caller must call .Close() on the returned ImageSource.
func (ref ostreeReference) NewImageSource(ctx *types.SystemContext) (types.ImageSource, error) {
	return nil, errors.New("Reading ostree: images is currently not supported")
}

// NewImageDestination returns a types.ImageDestination for this reference.
// The caller must call .Close() on the returned ImageDestination.
func (ref ostreeReference) NewImageDestination(ctx *types.SystemContext) (types.ImageDestination, error) {
	var tmpDir string
	if ctx == nil || ctx.OSTreeTmpDirPath == "" {
		tmpDir = os.TempDir()
	} else {
		tmpDir = ctx.OSTreeTmpDirPath
	}
	return newImageDestination(ref, tmpDir)
}

// DeleteImage deletes the named image from the registry, if supported.
func (ref ostreeReference) DeleteImage(ctx *types.SystemContext) error {
	return errors.Errorf("Deleting images not implemented for ostree: images")
}

var ostreeRefRegexp = regexp.MustCompile(`^[A-Za-z0-9.-]$`)

func encodeOStreeRef(in string) string {
	var buffer bytes.Buffer
	for i := range in {
		sub := in[i : i+1]
		if ostreeRefRegexp.MatchString(sub) {
			buffer.WriteString(sub)
		} else {
			buffer.WriteString(fmt.Sprintf("_%02X", sub[0]))
		}

	}
	return buffer.String()
}

// manifestPath returns a path for the manifest within a ostree using our conventions.
func (ref ostreeReference) manifestPath() string {
	return filepath.Join("manifest", "manifest.json")
}

// signaturePath returns a path for a signature within a ostree using our conventions.
func (ref ostreeReference) signaturePath(index int) string {
	return filepath.Join("manifest", fmt.Sprintf("signature-%d", index+1))
}
<commit_msg>ostree: use reference.ParseNormalizedNamed<commit_after>// +build !containers_image_ostree_stub

package ostree

import (
	"bytes"
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"strings"

	"github.com/pkg/errors"

	"github.com/containers/image/directory/explicitfilepath"
	"github.com/containers/image/docker/reference"
	"github.com/containers/image/transports"
	"github.com/containers/image/types"
)

const defaultOSTreeRepo = "/ostree/repo"

// Transport is an ImageTransport for ostree paths.
var Transport = ostreeTransport{}

type ostreeTransport struct{}

func (t ostreeTransport) Name() string {
	return "ostree"
}

func init() {
	transports.Register(Transport)
}

// ValidatePolicyConfigurationScope checks that scope is a valid name for a signature.PolicyTransportScopes keys
// (i.e. a valid PolicyConfigurationIdentity() or PolicyConfigurationNamespaces() return value).
// It is acceptable to allow an invalid value which will never be matched, it can "only" cause user confusion.
// scope passed to this function will not be "", that value is always allowed.
func (t ostreeTransport) ValidatePolicyConfigurationScope(scope string) error {
	sep := strings.Index(scope, ":")
	if sep < 0 {
		return errors.Errorf("Invalid ostree: scope %s: Must include a repo", scope)
	}
	repo := scope[:sep]

	if !strings.HasPrefix(repo, "/") {
		return errors.Errorf("Invalid ostree: scope %s: repository must be an absolute path", scope)
	}
	cleaned := filepath.Clean(repo)
	if cleaned != repo {
		return errors.Errorf(`Invalid ostree: scope %s: Uses non-canonical path format, perhaps try with path %s`, scope, cleaned)
	}

	// FIXME? In the namespaces within a repo,
	// we could be verifying the various character set and length restrictions
	// from docker/distribution/reference.regexp.go, but other than that there
	// are few semantically invalid strings.
	return nil
}

// ostreeReference is an ImageReference for ostree paths.
type ostreeReference struct {
	image      string
	branchName string
	repo       string
}

func (t ostreeTransport) ParseReference(ref string) (types.ImageReference, error) {
	var repo = ""
	var image = ""
	s := strings.SplitN(ref, "@/", 2)
	if len(s) == 1 {
		image, repo = s[0], defaultOSTreeRepo
	} else {
		image, repo = s[0], "/"+s[1]
	}

	return NewReference(image, repo)
}

// NewReference returns an OSTree reference for a specified repo and image.
func NewReference(image string, repo string) (types.ImageReference, error) {
	// image is not _really_ in a containers/image/docker/reference format;
	// as far as the libOSTree ociimage/* namespace is concerned, it is more or
	// less an arbitrary string with an implied tag.
	// Parse the image using reference.ParseNormalizedNamed so that we can
	// check whether the images has a tag specified and we can add ":latest" if needed
	ostreeImage, err := reference.ParseNormalizedNamed(image)
	if err != nil {
		return nil, err
	}

	if reference.IsNameOnly(ostreeImage) {
		image = image + ":latest"
	}

	resolved, err := explicitfilepath.ResolvePathToFullyExplicit(repo)
	if err != nil {
		// With os.IsNotExist(err), the parent directory of repo is also not existent;
		// that should ordinarily not happen, but it would be a bit weird to reject
		// references which do not specify a repo just because the implicit defaultOSTreeRepo
		// does not exist.
		if os.IsNotExist(err) && repo == defaultOSTreeRepo {
			resolved = repo
		} else {
			return nil, err
		}
	}
	// This is necessary to prevent directory paths returned by PolicyConfigurationNamespaces
	// from being ambiguous with values of PolicyConfigurationIdentity.
	if strings.Contains(resolved, ":") {
		return nil, errors.Errorf("Invalid OSTreeCI reference %s@%s: path %s contains a colon", image, repo, resolved)
	}

	return ostreeReference{
		image:      image,
		branchName: encodeOStreeRef(image),
		repo:       resolved,
	}, nil
}

func (ref ostreeReference) Transport() types.ImageTransport {
	return Transport
}

// StringWithinTransport returns a string representation of the reference, which MUST be such that
// reference.Transport().ParseReference(reference.StringWithinTransport()) returns an equivalent reference.
// NOTE: The returned string is not promised to be equal to the original input to ParseReference;
// e.g. default attribute values omitted by the user may be filled in in the return value, or vice versa.
// WARNING: Do not use the return value in the UI to describe an image, it does not contain the Transport().Name() prefix.
func (ref ostreeReference) StringWithinTransport() string {
	return fmt.Sprintf("%s@%s", ref.image, ref.repo)
}

// DockerReference returns a Docker reference associated with this reference
// (fully explicit, i.e. !reference.IsNameOnly, but reflecting user intent,
// not e.g. after redirect or alias processing), or nil if unknown/not applicable.
func (ref ostreeReference) DockerReference() reference.Named {
	return nil
}

func (ref ostreeReference) PolicyConfigurationIdentity() string {
	return fmt.Sprintf("%s:%s", ref.repo, ref.image)
}

// PolicyConfigurationNamespaces returns a list of other policy configuration namespaces to search
// for if explicit configuration for PolicyConfigurationIdentity() is not set.  The list will be processed
// in order, terminating on first match, and an implicit "" is always checked at the end.
// It is STRONGLY recommended for the first element, if any, to be a prefix of PolicyConfigurationIdentity(),
// and each following element to be a prefix of the element preceding it.
func (ref ostreeReference) PolicyConfigurationNamespaces() []string {
	s := strings.SplitN(ref.image, ":", 2)
	if len(s) != 2 { // Coverage: Should never happen, NewReference above ensures ref.image has a :tag.
		panic(fmt.Sprintf("Internal inconsistency: ref.image value %q does not have a :tag", ref.image))
	}
	name := s[0]
	res := []string{}
	for {
		res = append(res, fmt.Sprintf("%s:%s", ref.repo, name))

		lastSlash := strings.LastIndex(name, "/")
		if lastSlash == -1 {
			break
		}
		name = name[:lastSlash]
	}
	return res
}

// NewImage returns a types.Image for this reference, possibly specialized for this ImageTransport.
// The caller must call .Close() on the returned Image.
// NOTE: If any kind of signature verification should happen, build an UnparsedImage from the value returned by NewImageSource,
// verify that UnparsedImage, and convert it into a real Image via image.FromUnparsedImage.
func (ref ostreeReference) NewImage(ctx *types.SystemContext) (types.Image, error) {
	return nil, errors.New("Reading ostree: images is currently not supported")
}

// NewImageSource returns a types.ImageSource for this reference.
// The caller must call .Close() on the returned ImageSource.
func (ref ostreeReference) NewImageSource(ctx *types.SystemContext) (types.ImageSource, error) {
	return nil, errors.New("Reading ostree: images is currently not supported")
}

// NewImageDestination returns a types.ImageDestination for this reference.
// The caller must call .Close() on the returned ImageDestination.
func (ref ostreeReference) NewImageDestination(ctx *types.SystemContext) (types.ImageDestination, error) {
	var tmpDir string
	if ctx == nil || ctx.OSTreeTmpDirPath == "" {
		tmpDir = os.TempDir()
	} else {
		tmpDir = ctx.OSTreeTmpDirPath
	}
	return newImageDestination(ref, tmpDir)
}

// DeleteImage deletes the named image from the registry, if supported.
func (ref ostreeReference) DeleteImage(ctx *types.SystemContext) error {
	return errors.Errorf("Deleting images not implemented for ostree: images")
}

var ostreeRefRegexp = regexp.MustCompile(`^[A-Za-z0-9.-]$`)

func encodeOStreeRef(in string) string {
	var buffer bytes.Buffer
	for i := range in {
		sub := in[i : i+1]
		if ostreeRefRegexp.MatchString(sub) {
			buffer.WriteString(sub)
		} else {
			buffer.WriteString(fmt.Sprintf("_%02X", sub[0]))
		}

	}
	return buffer.String()
}

// manifestPath returns a path for the manifest within a ostree using our conventions.
func (ref ostreeReference) manifestPath() string {
	return filepath.Join("manifest", "manifest.json")
}

// signaturePath returns a path for a signature within a ostree using our conventions.
func (ref ostreeReference) signaturePath(index int) string {
	return filepath.Join("manifest", fmt.Sprintf("signature-%d", index+1))
}
<|endoftext|>


<commit_before>package worker

import (
	"fmt"
	proto "github.com/golang/protobuf/proto"
	"github.com/ohsu-comp-bio/funnel/proto/tes"
	"github.com/ohsu-comp-bio/funnel/util"
	"os"
	"path"
	"path/filepath"
	"strings"
)

// FileMapper is responsible for mapping paths into a working directory on the
// worker's host file system.
//
// Every task needs it's own directory to work in. When a file is downloaded for
// a task, it needs to be stored in the task's working directory. Similar for task
// outputs, uploads, stdin/out/err, etc. FileMapper helps the worker engine
// manage all these paths.
type FileMapper struct {
	Volumes []Volume
	Inputs  []*tes.TaskParameter
	Outputs []*tes.TaskParameter
	dir     string
}

// Volume represents a volume mounted into a docker container.
// This includes a HostPath, the path on the host file system,
// and a ContainerPath, the path on the container file system,
// and whether the volume is read-only.
type Volume struct {
	// The path in tes worker.
	HostPath string
	// The path in Docker.
	ContainerPath string
	Readonly      bool
}

// NewFileMapper returns a new FileMapper, which maps files into the given
// base directory.
func NewFileMapper(dir string) *FileMapper {
	// TODO error handling
	dir, _ = filepath.Abs(dir)
	return &FileMapper{
		Volumes: []Volume{},
		Inputs:  []*tes.TaskParameter{},
		Outputs: []*tes.TaskParameter{},
		dir:     dir,
	}
}

// MapTask adds all the volumes, inputs, and outputs in the given Task to the FileMapper.
func (mapper *FileMapper) MapTask(task *tes.Task) error {

	// Add all the volumes to the mapper
	for _, vol := range task.Volumes {
		err := mapper.AddTmpVolume(vol)
		if err != nil {
			return err
		}
	}

	// Add all the inputs to the mapper
	for _, input := range task.Inputs {
		err := mapper.AddInput(input)
		if err != nil {
			return err
		}
	}

	// Add all the outputs to the mapper
	for _, output := range task.Outputs {
		err := mapper.AddOutput(output)
		if err != nil {
			return err
		}
	}

	return nil
}

// AddVolume adds a mapped volume to the mapper. A corresponding Volume record
// is added to mapper.Volumes.
//
// If the volume paths are invalid or can't be mapped, an error is returned.
func (mapper *FileMapper) AddVolume(hostPath string, mountPoint string, readonly bool) error {
	vol := Volume{
		HostPath:      hostPath,
		ContainerPath: mountPoint,
		Readonly:      readonly,
	}

	if !mapper.hasVolume(vol) {
		mapper.Volumes = append(mapper.Volumes, vol)
	}
	return nil
}

// HostPath returns a mapped path.
//
// The path is concatenated to the mapper's base dir.
// e.g. If the mapper is configured with a base dir of "/tmp/mapped_files", then
// mapper.HostPath("/home/ubuntu/myfile") will return "/tmp/mapped_files/home/ubuntu/myfile".
//
// The mapped path is required to be a subpath of the mapper's base directory.
// e.g. mapper.HostPath("../../foo") should fail with an error.
func (mapper *FileMapper) HostPath(src string) (string, error) {
	p := path.Join(mapper.dir, src)
	p = path.Clean(p)
	if !mapper.IsSubpath(p, mapper.dir) {
		return "", fmt.Errorf("Invalid path: %s is not a valid subpath of %s", p, mapper.dir)
	}
	return p, nil
}

// OpenHostFile opens a file on the host file system at a mapped path.
// "src" is an unmapped path. This function will handle mapping the path.
//
// This function calls os.Open
//
// If the path can't be mapped or the file can't be opened, an error is returned.
func (mapper *FileMapper) OpenHostFile(src string) (*os.File, error) {
	p, perr := mapper.HostPath(src)
	if perr != nil {
		return nil, perr
	}
	f, oerr := os.Open(p)
	if oerr != nil {
		return nil, oerr
	}
	return f, nil
}

// CreateHostFile creates a file on the host file system at a mapped path.
// "src" is an unmapped path. This function will handle mapping the path.
//
// This function calls os.Create
//
// If the path can't be mapped or the file can't be created, an error is returned.
func (mapper *FileMapper) CreateHostFile(src string) (*os.File, error) {
	p, perr := mapper.HostPath(src)
	if perr != nil {
		return nil, perr
	}
	err := util.EnsurePath(p)
	if err != nil {
		return nil, err
	}
	f, oerr := os.Create(p)
	if oerr != nil {
		return nil, oerr
	}
	return f, nil
}

// AddTmpVolume creates a directory on the host based on the delcared path in
// the container and adds it to mapper.Volumes.
//
// If the path can't be mapped, an error is returned.
func (mapper *FileMapper) AddTmpVolume(mountPoint string) error {
	hostPath, err := mapper.HostPath(mountPoint)
	if err != nil {
		return err
	}

	err = util.EnsureDir(hostPath)
	if err != nil {
		return err
	}

	err = mapper.AddVolume(hostPath, mountPoint, false)
	if err != nil {
		return err
	}
	return nil
}

// AddInput adds an input to the mapped files for the given TaskParameter.
// A copy of the TaskParameter will be added to mapper.Inputs, with the
// "Path" field updated to the mapped host path.
//
// If the path can't be mapped an error is returned.
func (mapper *FileMapper) AddInput(input *tes.TaskParameter) error {
	hostPath, err := mapper.HostPath(input.Path)
	if err != nil {
		return err
	}

	err = util.EnsurePath(hostPath)
	if err != nil {
		return err
	}

	// Add input volumes
	err = mapper.AddVolume(hostPath, input.Path, true)
	if err != nil {
		return err
	}

	// Create a TaskParameter for the input with a path mapped to the host
	hostIn := proto.Clone(input).(*tes.TaskParameter)
	hostIn.Path = hostPath
	mapper.Inputs = append(mapper.Inputs, hostIn)
	return nil
}

// AddOutput adds an output to the mapped files for the given TaskParameter.
// A copy of the TaskParameter will be added to mapper.Outputs, with the
// "Path" field updated to the mapped host path.
//
// If the path can't be mapped, an error is returned.
func (mapper *FileMapper) AddOutput(output *tes.TaskParameter) error {
	hostPath, err := mapper.HostPath(output.Path)
	if err != nil {
		return err
	}

	hostDir := hostPath
	mountDir := output.Path
	if output.Type == tes.FileType_FILE {
		hostDir = path.Dir(hostPath)
		mountDir = path.Dir(output.Path)
	}

	err = util.EnsureDir(hostDir)
	if err != nil {
		return err
	}

	// Add output volumes
	err = mapper.AddVolume(hostDir, mountDir, false)
	if err != nil {
		return err
	}

	// Create a TaskParameter for the out with a path mapped to the host
	hostOut := proto.Clone(output).(*tes.TaskParameter)
	hostOut.Path = hostPath
	mapper.Outputs = append(mapper.Outputs, hostOut)
	return nil
}

// IsSubpath returns true if the given path "p" is a subpath of "base".
func (mapper *FileMapper) IsSubpath(p string, base string) bool {
	return strings.HasPrefix(p, base)
}

// check if a Volume is already present in the FileMapper
func (mapper *FileMapper) hasVolume(vol Volume) bool {
	for i, v := range mapper.Volumes {
		if vol == v {
			return true
		}

		// If the proposed RW Volume is not a subpath of an existing RW Volume
		// do not add it to the mapper
		// If an existing RW Volume is a subpath of the proposed RW Volume, replace it with
		// the proposed RW Volume
		if !vol.Readonly && !v.Readonly {
			if mapper.IsSubpath(vol.ContainerPath, v.ContainerPath) {
				return true
			} else if mapper.IsSubpath(v.ContainerPath, vol.ContainerPath) {
				mapper.Volumes[i] = vol
				return true
			}
		}
	}
	return false
}
<commit_msg>moved hasVolume logic into AddVolume<commit_after>package worker

import (
	"fmt"
	proto "github.com/golang/protobuf/proto"
	"github.com/ohsu-comp-bio/funnel/proto/tes"
	"github.com/ohsu-comp-bio/funnel/util"
	"os"
	"path"
	"path/filepath"
	"strings"
)

// FileMapper is responsible for mapping paths into a working directory on the
// worker's host file system.
//
// Every task needs it's own directory to work in. When a file is downloaded for
// a task, it needs to be stored in the task's working directory. Similar for task
// outputs, uploads, stdin/out/err, etc. FileMapper helps the worker engine
// manage all these paths.
type FileMapper struct {
	Volumes []Volume
	Inputs  []*tes.TaskParameter
	Outputs []*tes.TaskParameter
	dir     string
}

// Volume represents a volume mounted into a docker container.
// This includes a HostPath, the path on the host file system,
// and a ContainerPath, the path on the container file system,
// and whether the volume is read-only.
type Volume struct {
	// The path in tes worker.
	HostPath string
	// The path in Docker.
	ContainerPath string
	Readonly      bool
}

// NewFileMapper returns a new FileMapper, which maps files into the given
// base directory.
func NewFileMapper(dir string) *FileMapper {
	// TODO error handling
	dir, _ = filepath.Abs(dir)
	return &FileMapper{
		Volumes: []Volume{},
		Inputs:  []*tes.TaskParameter{},
		Outputs: []*tes.TaskParameter{},
		dir:     dir,
	}
}

// MapTask adds all the volumes, inputs, and outputs in the given Task to the FileMapper.
func (mapper *FileMapper) MapTask(task *tes.Task) error {

	// Add all the volumes to the mapper
	for _, vol := range task.Volumes {
		err := mapper.AddTmpVolume(vol)
		if err != nil {
			return err
		}
	}

	// Add all the inputs to the mapper
	for _, input := range task.Inputs {
		err := mapper.AddInput(input)
		if err != nil {
			return err
		}
	}

	// Add all the outputs to the mapper
	for _, output := range task.Outputs {
		err := mapper.AddOutput(output)
		if err != nil {
			return err
		}
	}

	return nil
}

// AddVolume adds a mapped volume to the mapper. A corresponding Volume record
// is added to mapper.Volumes.
//
// If the volume paths are invalid or can't be mapped, an error is returned.
func (mapper *FileMapper) AddVolume(hostPath string, mountPoint string, readonly bool) error {
	vol := Volume{
		HostPath:      hostPath,
		ContainerPath: mountPoint,
		Readonly:      readonly,
	}

	for i, v := range mapper.Volumes {
		// check if this volume is already present in the mapper
		if vol == v {
			return nil
		}

		// If the proposed RW Volume is not a subpath of an existing RW Volume
		// do not add it to the mapper
		// If an existing RW Volume is a subpath of the proposed RW Volume, replace it with
		// the proposed RW Volume
		if !vol.Readonly && !v.Readonly {
			if mapper.IsSubpath(vol.ContainerPath, v.ContainerPath) {
				return nil
			} else if mapper.IsSubpath(v.ContainerPath, vol.ContainerPath) {
				mapper.Volumes[i] = vol
				return nil
			}
		}
	}

	mapper.Volumes = append(mapper.Volumes, vol)
	return nil
}

// HostPath returns a mapped path.
//
// The path is concatenated to the mapper's base dir.
// e.g. If the mapper is configured with a base dir of "/tmp/mapped_files", then
// mapper.HostPath("/home/ubuntu/myfile") will return "/tmp/mapped_files/home/ubuntu/myfile".
//
// The mapped path is required to be a subpath of the mapper's base directory.
// e.g. mapper.HostPath("../../foo") should fail with an error.
func (mapper *FileMapper) HostPath(src string) (string, error) {
	p := path.Join(mapper.dir, src)
	p = path.Clean(p)
	if !mapper.IsSubpath(p, mapper.dir) {
		return "", fmt.Errorf("Invalid path: %s is not a valid subpath of %s", p, mapper.dir)
	}
	return p, nil
}

// OpenHostFile opens a file on the host file system at a mapped path.
// "src" is an unmapped path. This function will handle mapping the path.
//
// This function calls os.Open
//
// If the path can't be mapped or the file can't be opened, an error is returned.
func (mapper *FileMapper) OpenHostFile(src string) (*os.File, error) {
	p, perr := mapper.HostPath(src)
	if perr != nil {
		return nil, perr
	}
	f, oerr := os.Open(p)
	if oerr != nil {
		return nil, oerr
	}
	return f, nil
}

// CreateHostFile creates a file on the host file system at a mapped path.
// "src" is an unmapped path. This function will handle mapping the path.
//
// This function calls os.Create
//
// If the path can't be mapped or the file can't be created, an error is returned.
func (mapper *FileMapper) CreateHostFile(src string) (*os.File, error) {
	p, perr := mapper.HostPath(src)
	if perr != nil {
		return nil, perr
	}
	err := util.EnsurePath(p)
	if err != nil {
		return nil, err
	}
	f, oerr := os.Create(p)
	if oerr != nil {
		return nil, oerr
	}
	return f, nil
}

// AddTmpVolume creates a directory on the host based on the delcared path in
// the container and adds it to mapper.Volumes.
//
// If the path can't be mapped, an error is returned.
func (mapper *FileMapper) AddTmpVolume(mountPoint string) error {
	hostPath, err := mapper.HostPath(mountPoint)
	if err != nil {
		return err
	}

	err = util.EnsureDir(hostPath)
	if err != nil {
		return err
	}

	err = mapper.AddVolume(hostPath, mountPoint, false)
	if err != nil {
		return err
	}
	return nil
}

// AddInput adds an input to the mapped files for the given TaskParameter.
// A copy of the TaskParameter will be added to mapper.Inputs, with the
// "Path" field updated to the mapped host path.
//
// If the path can't be mapped an error is returned.
func (mapper *FileMapper) AddInput(input *tes.TaskParameter) error {
	hostPath, err := mapper.HostPath(input.Path)
	if err != nil {
		return err
	}

	err = util.EnsurePath(hostPath)
	if err != nil {
		return err
	}

	// Add input volumes
	err = mapper.AddVolume(hostPath, input.Path, true)
	if err != nil {
		return err
	}

	// Create a TaskParameter for the input with a path mapped to the host
	hostIn := proto.Clone(input).(*tes.TaskParameter)
	hostIn.Path = hostPath
	mapper.Inputs = append(mapper.Inputs, hostIn)
	return nil
}

// AddOutput adds an output to the mapped files for the given TaskParameter.
// A copy of the TaskParameter will be added to mapper.Outputs, with the
// "Path" field updated to the mapped host path.
//
// If the path can't be mapped, an error is returned.
func (mapper *FileMapper) AddOutput(output *tes.TaskParameter) error {
	hostPath, err := mapper.HostPath(output.Path)
	if err != nil {
		return err
	}

	hostDir := hostPath
	mountDir := output.Path
	if output.Type == tes.FileType_FILE {
		hostDir = path.Dir(hostPath)
		mountDir = path.Dir(output.Path)
	}

	err = util.EnsureDir(hostDir)
	if err != nil {
		return err
	}

	// Add output volumes
	err = mapper.AddVolume(hostDir, mountDir, false)
	if err != nil {
		return err
	}

	// Create a TaskParameter for the out with a path mapped to the host
	hostOut := proto.Clone(output).(*tes.TaskParameter)
	hostOut.Path = hostPath
	mapper.Outputs = append(mapper.Outputs, hostOut)
	return nil
}

// IsSubpath returns true if the given path "p" is a subpath of "base".
func (mapper *FileMapper) IsSubpath(p string, base string) bool {
	return strings.HasPrefix(p, base)
}
<|endoftext|>


<commit_before>package workspace

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"strings"
	"time"
)

const solutionFilename = "solution.json"
const legacySolutionFilename = ".solution.json"
const ignoreSubdir = ".exercism"

var metadataFilepath = filepath.Join(ignoreSubdir, solutionFilename)

// Solution contains metadata about a user's solution.
type Solution struct {
	Track       string     `json:"track"`
	Exercise    string     `json:"exercise"`
	ID          string     `json:"id"`
	Team        string     `json:"team,omitempty"`
	URL         string     `json:"url"`
	Handle      string     `json:"handle"`
	IsRequester bool       `json:"is_requester"`
	SubmittedAt *time.Time `json:"submitted_at,omitempty"`
	Dir         string     `json:"-"`
	AutoApprove bool       `json:"auto_approve"`
}

// NewSolution reads solution metadata from a file in the given directory.
func NewSolution(dir string) (*Solution, error) {
	path := filepath.Join(dir, metadataFilepath)
	b, err := ioutil.ReadFile(path)
	if err != nil {
		return &Solution{}, err
	}
	var s Solution
	if err := json.Unmarshal(b, &s); err != nil {
		return &Solution{}, err
	}
	s.Dir = dir
	return &s, nil
}

// Suffix is the serial numeric value appended to an exercise directory.
// This is appended to avoid name conflicts, and does not indicate a particular
// iteration.
func (s *Solution) Suffix() string {
	return strings.Trim(strings.Replace(filepath.Base(s.Dir), s.Exercise, "", 1), "-.")
}

func (s *Solution) String() string {
	str := fmt.Sprintf("%s/%s", s.Track, s.Exercise)
	if s.Suffix() != "" {
		str = fmt.Sprintf("%s (%s)", str, s.Suffix())
	}
	if !s.IsRequester && s.Handle != "" {
		str = fmt.Sprintf("%s by @%s", str, s.Handle)
	}
	return str
}

// Write stores solution metadata to a file.
func (s *Solution) Write(dir string) error {
	b, err := json.Marshal(s)
	if err != nil {
		return err
	}
	if err = os.MkdirAll(filepath.Join(dir, ignoreSubdir), os.FileMode(0755)); err != nil {
		return err
	}
	if err = ioutil.WriteFile(NewExerciseFromDir(dir).MetadataFilepath(), b,
		os.FileMode(0600)); err != nil {
		return err
	}
	s.Dir = dir
	return nil
}

// PathToParent is the relative path from the workspace to the parent dir.
func (s *Solution) PathToParent() string {
	var dir string
	if !s.IsRequester {
		dir = filepath.Join("users")
	}
	return filepath.Join(dir, s.Track)
}
<commit_msg>Refactor simplify solution.Write<commit_after>package workspace

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"strings"
	"time"
)

const solutionFilename = "solution.json"
const legacySolutionFilename = ".solution.json"
const ignoreSubdir = ".exercism"

var metadataFilepath = filepath.Join(ignoreSubdir, solutionFilename)

// Solution contains metadata about a user's solution.
type Solution struct {
	Track       string     `json:"track"`
	Exercise    string     `json:"exercise"`
	ID          string     `json:"id"`
	Team        string     `json:"team,omitempty"`
	URL         string     `json:"url"`
	Handle      string     `json:"handle"`
	IsRequester bool       `json:"is_requester"`
	SubmittedAt *time.Time `json:"submitted_at,omitempty"`
	Dir         string     `json:"-"`
	AutoApprove bool       `json:"auto_approve"`
}

// NewSolution reads solution metadata from a file in the given directory.
func NewSolution(dir string) (*Solution, error) {
	path := filepath.Join(dir, metadataFilepath)
	b, err := ioutil.ReadFile(path)
	if err != nil {
		return &Solution{}, err
	}
	var s Solution
	if err := json.Unmarshal(b, &s); err != nil {
		return &Solution{}, err
	}
	s.Dir = dir
	return &s, nil
}

// Suffix is the serial numeric value appended to an exercise directory.
// This is appended to avoid name conflicts, and does not indicate a particular
// iteration.
func (s *Solution) Suffix() string {
	return strings.Trim(strings.Replace(filepath.Base(s.Dir), s.Exercise, "", 1), "-.")
}

func (s *Solution) String() string {
	str := fmt.Sprintf("%s/%s", s.Track, s.Exercise)
	if s.Suffix() != "" {
		str = fmt.Sprintf("%s (%s)", str, s.Suffix())
	}
	if !s.IsRequester && s.Handle != "" {
		str = fmt.Sprintf("%s by @%s", str, s.Handle)
	}
	return str
}

// Write stores solution metadata to a file.
func (s *Solution) Write(dir string) error {
	b, err := json.Marshal(s)
	if err != nil {
		return err
	}
	metadataAbsoluteFilepath := NewExerciseFromDir(dir).MetadataFilepath()
	if err = os.MkdirAll(filepath.Dir(metadataAbsoluteFilepath), os.FileMode(0755)); err != nil {
		return err
	}
	if err = ioutil.WriteFile(metadataAbsoluteFilepath, b, os.FileMode(0600)); err != nil {
		return err
	}
	s.Dir = dir
	return nil
}

// PathToParent is the relative path from the workspace to the parent dir.
func (s *Solution) PathToParent() string {
	var dir string
	if !s.IsRequester {
		dir = filepath.Join("users")
	}
	return filepath.Join(dir, s.Track)
}
<|endoftext|>


<commit_before>package autonat

import (
	inet "github.com/libp2p/go-libp2p-net"
	peer "github.com/libp2p/go-libp2p-peer"
	ma "github.com/multiformats/go-multiaddr"
)

var _ inet.Notifiee = (*AutoNATState)(nil)

func (as *AutoNATState) Listen(net inet.Network, a ma.Multiaddr)      {}
func (as *AutoNATState) ListenClose(net inet.Network, a ma.Multiaddr) {}
func (as *AutoNATState) OpenedStream(net inet.Network, s inet.Stream) {}
func (as *AutoNATState) ClosedStream(net inet.Network, s inet.Stream) {}

func (as *AutoNATState) Connected(net inet.Network, c inet.Conn) {
	go func(p peer.ID) {
		s, err := as.host.NewStream(as.ctx, p, AutoNATProto)
		if err != nil {
			return
		}
		s.Close()

		log.Infof("Discovered AutoNAT peer %s", p.Pretty())
		as.mx.Lock()
		as.peers[p] = true
		as.mx.Unlock()
	}(c.RemotePeer())
}

func (as *AutoNATState) Disconnected(net inet.Network, c inet.Conn) {
	as.mx.Lock()
	delete(as.peers, c.RemotePeer())
	as.mx.Unlock()
}
<commit_msg>don't delete autonat peers on disconnect, just mark them as disconnected<commit_after>package autonat

import (
	inet "github.com/libp2p/go-libp2p-net"
	peer "github.com/libp2p/go-libp2p-peer"
	ma "github.com/multiformats/go-multiaddr"
)

var _ inet.Notifiee = (*AutoNATState)(nil)

func (as *AutoNATState) Listen(net inet.Network, a ma.Multiaddr)      {}
func (as *AutoNATState) ListenClose(net inet.Network, a ma.Multiaddr) {}
func (as *AutoNATState) OpenedStream(net inet.Network, s inet.Stream) {}
func (as *AutoNATState) ClosedStream(net inet.Network, s inet.Stream) {}

func (as *AutoNATState) Connected(net inet.Network, c inet.Conn) {
	go func(p peer.ID) {
		s, err := as.host.NewStream(as.ctx, p, AutoNATProto)
		if err != nil {
			return
		}
		s.Close()

		log.Infof("Discovered AutoNAT peer %s", p.Pretty())
		as.mx.Lock()
		as.peers[p] = true
		as.mx.Unlock()
	}(c.RemotePeer())
}

func (as *AutoNATState) Disconnected(net inet.Network, c inet.Conn) {
	as.mx.Lock()
	as.peers[c.RemotePeer()] = false
	as.mx.Unlock()
}
<|endoftext|>


<commit_before>package hook

import (
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"runtime"

	"github.com/buildkite/agent/v3/bootstrap/shell"
	"github.com/buildkite/agent/v3/env"
	"github.com/buildkite/agent/v3/utils"
)

const (
	hookExitStatusEnv = `BUILDKITE_HOOK_EXIT_STATUS`
	hookWorkingDirEnv = `BUILDKITE_HOOK_WORKING_DIR`
)

// Hooks get "sourced" into the bootstrap in the sense that they get the
// environment set for them and then we capture any extra environment variables
// that are exported in the script.

// The tricky thing is that it's impossible to grab the ENV of a child process
// before it finishes, so we've got an awesome (ugly) hack to get around this.
// We write the ENV to file, run the hook and then write the ENV back to another file.
// Then we can use the diff of the two to figure out what changes to make to the
// bootstrap. Horrible, but effective.

// ScriptWrapper wraps a hook script with env collection and then provides
// a way to get the difference between the environment before the hook is run and
// after it
type ScriptWrapper struct {
	hookPath      string
	scriptFile    *os.File
	beforeEnvFile *os.File
	afterEnvFile  *os.File
	beforeWd      string
}

type HookScriptChanges struct {
	Diff env.Diff
	Dir string
}

// CreateScriptWrapper creates and configures a ScriptWrapper.
// Writes temporary files to the filesystem.
func CreateScriptWrapper(hookPath string) (*ScriptWrapper, error) {
	var wrap = &ScriptWrapper{
		hookPath: hookPath,
	}

	var err error
	var scriptFileName string = `buildkite-agent-bootstrap-hook-runner`
	var isBashHook bool
	var isPwshHook bool
	var isWindows = runtime.GOOS == "windows"

	// we use bash hooks for scripts with no extension, otherwise on windows
	// we probably need a .bat extension
	if filepath.Ext(hookPath) == ".ps1" {
		isPwshHook = true
		scriptFileName += ".ps1"
	} else if filepath.Ext(hookPath) == "" {
		isBashHook = true
	} else if isWindows {
		scriptFileName += ".bat"
	}

	// Create a temporary file that we'll put the hook runner code in
	wrap.scriptFile, err = shell.TempFileWithExtension(scriptFileName)
	if err != nil {
		return nil, err
	}
	defer wrap.scriptFile.Close()

	// We'll pump the ENV before the hook into this temp file
	wrap.beforeEnvFile, err = shell.TempFileWithExtension(
		`buildkite-agent-bootstrap-hook-env-before`,
	)
	if err != nil {
		return nil, err
	}
	wrap.beforeEnvFile.Close()

	// We'll then pump the ENV _after_ the hook into this temp file
	wrap.afterEnvFile, err = shell.TempFileWithExtension(
		`buildkite-agent-bootstrap-hook-env-after`,
	)
	if err != nil {
		return nil, err
	}
	wrap.afterEnvFile.Close()

	absolutePathToHook, err := filepath.Abs(wrap.hookPath)
	if err != nil {
		return nil, fmt.Errorf("Failed to find absolute path to \"%s\" (%s)", wrap.hookPath, err)
	}

	wrap.beforeWd, err = os.Getwd()
	if err != nil {
		return nil, err
	}

	// Create the hook runner code
	var script string
	if isWindows && !isBashHook && !isPwshHook {
		script = "@echo off\n" +
			"SETLOCAL ENABLEDELAYEDEXPANSION\n" +
			"SET > \"" + wrap.beforeEnvFile.Name() + "\"\n" +
			"CALL \"" + absolutePathToHook + "\"\n" +
			"SET " + hookExitStatusEnv + "=!ERRORLEVEL!\n" +
			"SET " + hookWorkingDirEnv + "=%CD%\n" +
			"SET > \"" + wrap.afterEnvFile.Name() + "\"\n" +
			"EXIT %" + hookExitStatusEnv + "%"
	} else if isWindows && isPwshHook {
		script = `$ErrorActionPreference = "STOP"` + "\n" +
			`Get-ChildItem Env: | Foreach-Object {"$($_.Name)=$($_.Value)"} | Set-Content "` + wrap.beforeEnvFile.Name() + `"` + "\n" +
			absolutePathToHook + "\n" +
			`if ($LASTEXITCODE -eq $null) {$Env:` + hookExitStatusEnv + ` = 0} else {$Env:` + hookExitStatusEnv + ` = $LASTEXITCODE}` + "\n" +
			`$Env:` + hookWorkingDirEnv + ` = $PWD | Select-Object -ExpandProperty Path` + "\n" +
			`Get-ChildItem Env: | Foreach-Object {"$($_.Name)=$($_.Value)"} | Set-Content "` + wrap.afterEnvFile.Name() + `"` + "\n" +
			`exit $Env:` + hookExitStatusEnv
	} else {
		script = "export -p > \"" + filepath.ToSlash(wrap.beforeEnvFile.Name()) + "\"\n" +
			". \"" + filepath.ToSlash(absolutePathToHook) + "\"\n" +
			"export " + hookExitStatusEnv + "=$?\n" +
			"export " + hookWorkingDirEnv + "=$PWD\n" +
			"export -p > \"" + filepath.ToSlash(wrap.afterEnvFile.Name()) + "\"\n" +
			"exit $" + hookExitStatusEnv
	}

	// Write the hook script to the runner then close the file so we can run it
	_, err = wrap.scriptFile.WriteString(script)
	if err != nil {
		return nil, err
	}

	// Make script executable
	err = utils.ChmodExecutable(wrap.scriptFile.Name())
	if err != nil {
		return wrap, err
	}

	return wrap, nil
}

// Path returns the path to the wrapper script, this is the one that should be executed
func (wrap *ScriptWrapper) Path() string {
	return wrap.scriptFile.Name()
}

// Close cleans up the wrapper script and the environment files
func (wrap *ScriptWrapper) Close() {
	os.Remove(wrap.scriptFile.Name())
	os.Remove(wrap.beforeEnvFile.Name())
	os.Remove(wrap.afterEnvFile.Name())
}

// Changes returns the changes in the environment and working dir after the hook script runs
func (wrap *ScriptWrapper) Changes() (HookScriptChanges, error) {
	beforeEnvContents, err := ioutil.ReadFile(wrap.beforeEnvFile.Name())
	if err != nil {
		return HookScriptChanges{}, fmt.Errorf("Failed to read \"%s\" (%s)", wrap.beforeEnvFile.Name(), err)
	}

	afterEnvContents, err := ioutil.ReadFile(wrap.afterEnvFile.Name())
	if err != nil {
		return HookScriptChanges{}, fmt.Errorf("Failed to read \"%s\" (%s)", wrap.afterEnvFile.Name(), err)
	}

	beforeEnv := env.FromExport(string(beforeEnvContents))

	afterEnv := env.FromExport(string(afterEnvContents))
	if afterEnv.Length() == 0 {
		// If the after env is completely empty it is likely because the hook
		// ran exit(). Instead of falling over and breaking any subsequent
		// commands, lets fall back to the original before env since we can’t
		// extract a meaningful diff.
		afterEnv = beforeEnv
	}

	diff := afterEnv.Diff(beforeEnv)

	wd := wrap.getAfterWd(diff)

	diff.Remove(hookExitStatusEnv)
	diff.Remove(hookWorkingDirEnv)

	// Bash sets this, but we don't care about it
	diff.Remove("_")

	return HookScriptChanges{Diff: diff, Dir: wd}, nil
}

func (wrap *ScriptWrapper) getAfterWd(diff env.Diff) string {
	addedVar, ok := diff.Added[hookWorkingDirEnv]
	if ok {
		return addedVar
	}

	changedVar, ok := diff.Changed[hookWorkingDirEnv]
	if ok {
		return changedVar.New
	}

	return wrap.beforeWd
}
<commit_msg>Don't return the current process' cwd as the hook's after wd<commit_after>package hook

import (
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"runtime"

	"github.com/buildkite/agent/v3/bootstrap/shell"
	"github.com/buildkite/agent/v3/env"
	"github.com/buildkite/agent/v3/utils"
)

const (
	hookExitStatusEnv = `BUILDKITE_HOOK_EXIT_STATUS`
	hookWorkingDirEnv = `BUILDKITE_HOOK_WORKING_DIR`
)

// Hooks get "sourced" into the bootstrap in the sense that they get the
// environment set for them and then we capture any extra environment variables
// that are exported in the script.

// The tricky thing is that it's impossible to grab the ENV of a child process
// before it finishes, so we've got an awesome (ugly) hack to get around this.
// We write the ENV to file, run the hook and then write the ENV back to another file.
// Then we can use the diff of the two to figure out what changes to make to the
// bootstrap. Horrible, but effective.

// ScriptWrapper wraps a hook script with env collection and then provides
// a way to get the difference between the environment before the hook is run and
// after it
type ScriptWrapper struct {
	hookPath      string
	scriptFile    *os.File
	beforeEnvFile *os.File
	afterEnvFile  *os.File
}

type HookScriptChanges struct {
	Diff env.Diff
	Dir string
}

// CreateScriptWrapper creates and configures a ScriptWrapper.
// Writes temporary files to the filesystem.
func CreateScriptWrapper(hookPath string) (*ScriptWrapper, error) {
	var wrap = &ScriptWrapper{
		hookPath: hookPath,
	}

	var err error
	var scriptFileName string = `buildkite-agent-bootstrap-hook-runner`
	var isBashHook bool
	var isPwshHook bool
	var isWindows = runtime.GOOS == "windows"

	// we use bash hooks for scripts with no extension, otherwise on windows
	// we probably need a .bat extension
	if filepath.Ext(hookPath) == ".ps1" {
		isPwshHook = true
		scriptFileName += ".ps1"
	} else if filepath.Ext(hookPath) == "" {
		isBashHook = true
	} else if isWindows {
		scriptFileName += ".bat"
	}

	// Create a temporary file that we'll put the hook runner code in
	wrap.scriptFile, err = shell.TempFileWithExtension(scriptFileName)
	if err != nil {
		return nil, err
	}
	defer wrap.scriptFile.Close()

	// We'll pump the ENV before the hook into this temp file
	wrap.beforeEnvFile, err = shell.TempFileWithExtension(
		`buildkite-agent-bootstrap-hook-env-before`,
	)
	if err != nil {
		return nil, err
	}
	wrap.beforeEnvFile.Close()

	// We'll then pump the ENV _after_ the hook into this temp file
	wrap.afterEnvFile, err = shell.TempFileWithExtension(
		`buildkite-agent-bootstrap-hook-env-after`,
	)
	if err != nil {
		return nil, err
	}
	wrap.afterEnvFile.Close()

	absolutePathToHook, err := filepath.Abs(wrap.hookPath)
	if err != nil {
		return nil, fmt.Errorf("Failed to find absolute path to \"%s\" (%s)", wrap.hookPath, err)
	}

	// Create the hook runner code
	var script string
	if isWindows && !isBashHook && !isPwshHook {
		script = "@echo off\n" +
			"SETLOCAL ENABLEDELAYEDEXPANSION\n" +
			"SET > \"" + wrap.beforeEnvFile.Name() + "\"\n" +
			"CALL \"" + absolutePathToHook + "\"\n" +
			"SET " + hookExitStatusEnv + "=!ERRORLEVEL!\n" +
			"SET " + hookWorkingDirEnv + "=%CD%\n" +
			"SET > \"" + wrap.afterEnvFile.Name() + "\"\n" +
			"EXIT %" + hookExitStatusEnv + "%"
	} else if isWindows && isPwshHook {
		script = `$ErrorActionPreference = "STOP"` + "\n" +
			`Get-ChildItem Env: | Foreach-Object {"$($_.Name)=$($_.Value)"} | Set-Content "` + wrap.beforeEnvFile.Name() + `"` + "\n" +
			absolutePathToHook + "\n" +
			`if ($LASTEXITCODE -eq $null) {$Env:` + hookExitStatusEnv + ` = 0} else {$Env:` + hookExitStatusEnv + ` = $LASTEXITCODE}` + "\n" +
			`$Env:` + hookWorkingDirEnv + ` = $PWD | Select-Object -ExpandProperty Path` + "\n" +
			`Get-ChildItem Env: | Foreach-Object {"$($_.Name)=$($_.Value)"} | Set-Content "` + wrap.afterEnvFile.Name() + `"` + "\n" +
			`exit $Env:` + hookExitStatusEnv
	} else {
		script = "export -p > \"" + filepath.ToSlash(wrap.beforeEnvFile.Name()) + "\"\n" +
			". \"" + filepath.ToSlash(absolutePathToHook) + "\"\n" +
			"export " + hookExitStatusEnv + "=$?\n" +
			"export " + hookWorkingDirEnv + "=$PWD\n" +
			"export -p > \"" + filepath.ToSlash(wrap.afterEnvFile.Name()) + "\"\n" +
			"exit $" + hookExitStatusEnv
	}

	// Write the hook script to the runner then close the file so we can run it
	_, err = wrap.scriptFile.WriteString(script)
	if err != nil {
		return nil, err
	}

	// Make script executable
	err = utils.ChmodExecutable(wrap.scriptFile.Name())
	if err != nil {
		return wrap, err
	}

	return wrap, nil
}

// Path returns the path to the wrapper script, this is the one that should be executed
func (wrap *ScriptWrapper) Path() string {
	return wrap.scriptFile.Name()
}

// Close cleans up the wrapper script and the environment files
func (wrap *ScriptWrapper) Close() {
	os.Remove(wrap.scriptFile.Name())
	os.Remove(wrap.beforeEnvFile.Name())
	os.Remove(wrap.afterEnvFile.Name())
}

// Changes returns the changes in the environment and working dir after the hook script runs
func (wrap *ScriptWrapper) Changes() (HookScriptChanges, error) {
	beforeEnvContents, err := ioutil.ReadFile(wrap.beforeEnvFile.Name())
	if err != nil {
		return HookScriptChanges{}, fmt.Errorf("Failed to read \"%s\" (%s)", wrap.beforeEnvFile.Name(), err)
	}

	afterEnvContents, err := ioutil.ReadFile(wrap.afterEnvFile.Name())
	if err != nil {
		return HookScriptChanges{}, fmt.Errorf("Failed to read \"%s\" (%s)", wrap.afterEnvFile.Name(), err)
	}

	beforeEnv := env.FromExport(string(beforeEnvContents))

	afterEnv := env.FromExport(string(afterEnvContents))
	if afterEnv.Length() == 0 {
		// If the after env is completely empty it is likely because the hook
		// ran exit(). Instead of falling over and breaking any subsequent
		// commands, lets fall back to the original before env since we can’t
		// extract a meaningful diff.
		afterEnv = beforeEnv
	}

	diff := afterEnv.Diff(beforeEnv)

	wd, _ := wrap.getAfterWd(diff)

	diff.Remove(hookExitStatusEnv)
	diff.Remove(hookWorkingDirEnv)

	// Bash sets this, but we don't care about it
	diff.Remove("_")

	return HookScriptChanges{Diff: diff, Dir: wd}, nil
}

func (wrap *ScriptWrapper) getAfterWd(diff env.Diff) (string, err) {
	addedVar, ok := diff.Added[hookWorkingDirEnv]
	if ok {
		return addedVar, nil
	}

	changedVar, ok := diff.Changed[hookWorkingDirEnv]
	if ok {
		return changedVar.New, nil
	}

	return "", fmt.Errorf("%q was not present in the hook after environment", hookWorkingDirEnv)
}
<|endoftext|>


<commit_before>package app

import (
	"fmt"
	"github.com/ionous/sashimi/console"
	E "github.com/ionous/sashimi/event"
	"github.com/ionous/sashimi/meta"
	"github.com/ionous/sashimi/net/resource"
	"github.com/ionous/sashimi/runtime/api"
	"github.com/ionous/sashimi/util/ident"
	"os"
)

// CommandOutput records game state changes, gets sent to the player/client.
type CommandOutput struct {
	id     string
	view   View
	events *EventStream
	serial *ObjSerializer
	text   console.BufferedOutput
}

func NewCommandOutput(id string, m meta.Model, view View) *CommandOutput {
	return &CommandOutput{
		id:     id,
		view:   view,
		events: NewEventStream(),
		serial: NewObjSerializer(m, resource.NewObjectList()),
	}
}

// ActorSays adds a command for an actor's line of dialog.
func (out *CommandOutput) ActorSays(gobj meta.Instance, lines []string) {
	if !out.view.InView(gobj) {
		out.Log(fmt.Sprintf("CommandOutput: actor '%s' not in view, ignoring speech: (%v)\n", gobj.GetId(), lines))
	} else {
		out.flushPending()
		tgt := NewObjectRef(gobj)
		out.events.AddAction("say", tgt, lines)
	}
}

// ScriptSays adds a command for passed script lines.
// ( The implementation actually consolidates consecutive script says into a single command.
// which gets written during flushPending() )
func (out *CommandOutput) ScriptSays(lines []string) {
	for _, l := range lines {
		out.text.Println(l)
	}
}

func (out *CommandOutput) BeginEvent(tgt E.ITarget, _ E.PathList, msg *E.Message) api.IEndEvent {
	out.flushPending()
	// msg.Data == RunTimeAction
	// theres not really parameters for events right now
	// other than tgt, src, ctx right now.
	out.events.PushEvent(msg.Id, tgt, nil)
	return out
}

func (out *CommandOutput) EndEvent() {
	out.flushPending()
	out.events.PopEvent()
}

// Log the passed message locally, doesn't generate a client command.
// FIX: a log interface -- perhaps as an anonymous member -- then we could have logf, etc.
func (out *CommandOutput) Log(message string) {
	os.Stderr.WriteString(message)
}

// FlushDocument containing all commands to the passed document builder.
func (out *CommandOutput) FlushDocument(doc resource.DocumentBuilder) {
	out.flushPending()
	game := doc.NewObject(out.id, "game")
	if events := out.events.Flush(); len(events) > 0 {
		game.SetAttr("events", events)
	}
	doc.SetIncluded(out.serial.out)
}

func (out *CommandOutput) NumChange(gobj meta.Instance, prop ident.Id, prev, next float32) {
	if !out.view.InView(gobj) {
		out.Log(fmt.Sprintf("CommandOutput: '%s' not in view,ignoring num change %s(%s->%s)\n", gobj.GetId(), prop, prev, next))
	} else {
		obj := NewObjectRef(gobj)
		data := struct {
			Prop  string  `json:"prop"`
			Value float32 `json:"value"`
		}{jsonId(prop), next}
		out.events.AddAction("x-num", obj, data)
	}
}

func (out *CommandOutput) TextChange(gobj meta.Instance, prop ident.Id, prev, next string) {
	if !out.view.InView(gobj) {
		out.Log(fmt.Sprintf("CommandOutput: '%s' not in view(%s), ignoring text change %s(%s->%s)\n", gobj.GetId(), prop, prev, next))
	} else {
		obj := NewObjectRef(gobj)
		data := struct {
			Prop  string `json:"prop"`
			Value string `json:"value"`
		}{jsonId(prop), next}
		out.events.AddAction("x-txt", obj, data)
	}
}

func (out *CommandOutput) StateChange(gobj meta.Instance, prop ident.Id, prev, next ident.Id) {
	if !out.view.InView(gobj) {
		out.Log(fmt.Sprintf("CommandOutput: '%s' not in view, ignoring state change %s(%s->%s)\n", gobj.GetId(), prop, prev, next))
	} else {
		obj := NewObjectRef(gobj)
		data := struct {
			Prop string `json:"prop"`
			Prev string `json:"prev"`
			Next string `json:"next"`
		}{jsonId(prop),
			jsonId(prev),
			jsonId(next)}
		out.events.AddAction("x-set", obj, data)
	}
}

// currently sent on the "one" side of an object for any object value.
// ( ex. actor.whereabouts; not: room.contents. )
func (out *CommandOutput) ReferenceChange(gobj meta.Instance, prop, other ident.Id, prev, next meta.Instance) {
	if out.view.Viewpoint() == gobj {
		var n ident.Id
		if next != nil {
			n = next.GetId()
		}
		out.Log(fmt.Sprintf("CommandOutput: changing view to %s\n", n))
		if out.view.ChangedView(gobj, prop, next) && next != nil {
			out.serial.Include(next)
		}
		if next != nil && !out.view.InView(next) {
			panic("KJKJ")
		}
	} else {
		if out.view.EnteredView(gobj, prop, next) {
			out.serial.Include(gobj)
		}
	}
	relatedView := out.view.InView(gobj) ||
		(prev != nil && out.view.InView(prev)) ||
		(next != nil && out.view.InView(next))

	if !relatedView {
		var p, n ident.Id
		if prev != nil {
			p = prev.GetId()
		}
		if next != nil {
			n = next.GetId()
		}
		out.Log(fmt.Sprintf("CommandOutput: '%s' not in view, ignoring refchange %v(%v->%v)\n", gobj.GetId(), prop, p, n))
	} else {
		obj := NewObjectRef(gobj)

		relChange := struct {
			Prop  string           `json:"prop"`
			Other string           `json:"other"`
			Prev  *resource.Object `json:"prev,omitempty"`
			Next  *resource.Object `json:"next,omitempty"`
		}{Prop: jsonId(prop), Other: jsonId(other)}

		if prev != nil {
			relChange.Prev = NewObjectRef(prev)
		}
		if next != nil {
			relChange.Next = NewObjectRef(next)
		}

		out.events.AddAction("x-rel", obj, relChange)
	}
}

// flushPending buffered lines into the fake display object.
// ( so long as theres a flush before push and pop. )
func (out *CommandOutput) flushPending() {
	if lines := out.text.Flush(); len(lines) > 0 {
		if !EmptyLines(lines) {
			tgt := resource.NewObject("_display_", "_sys_")
			out.events.AddAction("print", tgt, lines)
		}
	}
}

// FXIXI some code -- like report the view --
// prettifies the output by printing blank lines (ugh)
func EmptyLines(lines []string) bool {
	empty := true
	for _, l := range lines {
		if len(l) > 0 {
			empty = false
			break
		}
	}
	return empty
}
<commit_msg>remove test panic<commit_after>package app

import (
	"fmt"
	"github.com/ionous/sashimi/console"
	E "github.com/ionous/sashimi/event"
	"github.com/ionous/sashimi/meta"
	"github.com/ionous/sashimi/net/resource"
	"github.com/ionous/sashimi/runtime/api"
	"github.com/ionous/sashimi/util/ident"
	"os"
)

// CommandOutput records game state changes, gets sent to the player/client.
type CommandOutput struct {
	id     string
	view   View
	events *EventStream
	serial *ObjSerializer
	text   console.BufferedOutput
}

func NewCommandOutput(id string, m meta.Model, view View) *CommandOutput {
	return &CommandOutput{
		id:     id,
		view:   view,
		events: NewEventStream(),
		serial: NewObjSerializer(m, resource.NewObjectList()),
	}
}

// ActorSays adds a command for an actor's line of dialog.
func (out *CommandOutput) ActorSays(gobj meta.Instance, lines []string) {
	if !out.view.InView(gobj) {
		out.Log(fmt.Sprintf("CommandOutput: actor '%s' not in view, ignoring speech: (%v)\n", gobj.GetId(), lines))
	} else {
		out.flushPending()
		tgt := NewObjectRef(gobj)
		out.events.AddAction("say", tgt, lines)
	}
}

// ScriptSays adds a command for passed script lines.
// ( The implementation actually consolidates consecutive script says into a single command.
// which gets written during flushPending() )
func (out *CommandOutput) ScriptSays(lines []string) {
	for _, l := range lines {
		out.text.Println(l)
	}
}

func (out *CommandOutput) BeginEvent(tgt E.ITarget, _ E.PathList, msg *E.Message) api.IEndEvent {
	out.flushPending()
	// msg.Data == RunTimeAction
	// theres not really parameters for events right now
	// other than tgt, src, ctx right now.
	out.events.PushEvent(msg.Id, tgt, nil)
	return out
}

func (out *CommandOutput) EndEvent() {
	out.flushPending()
	out.events.PopEvent()
}

// Log the passed message locally, doesn't generate a client command.
// FIX: a log interface -- perhaps as an anonymous member -- then we could have logf, etc.
func (out *CommandOutput) Log(message string) {
	os.Stderr.WriteString(message)
}

// FlushDocument containing all commands to the passed document builder.
func (out *CommandOutput) FlushDocument(doc resource.DocumentBuilder) {
	out.flushPending()
	game := doc.NewObject(out.id, "game")
	if events := out.events.Flush(); len(events) > 0 {
		game.SetAttr("events", events)
	}
	doc.SetIncluded(out.serial.out)
}

func (out *CommandOutput) NumChange(gobj meta.Instance, prop ident.Id, prev, next float32) {
	if !out.view.InView(gobj) {
		out.Log(fmt.Sprintf("CommandOutput: '%s' not in view,ignoring num change %s(%s->%s)\n", gobj.GetId(), prop, prev, next))
	} else {
		obj := NewObjectRef(gobj)
		data := struct {
			Prop  string  `json:"prop"`
			Value float32 `json:"value"`
		}{jsonId(prop), next}
		out.events.AddAction("x-num", obj, data)
	}
}

func (out *CommandOutput) TextChange(gobj meta.Instance, prop ident.Id, prev, next string) {
	if !out.view.InView(gobj) {
		out.Log(fmt.Sprintf("CommandOutput: '%s' not in view(%s), ignoring text change %s(%s->%s)\n", gobj.GetId(), prop, prev, next))
	} else {
		obj := NewObjectRef(gobj)
		data := struct {
			Prop  string `json:"prop"`
			Value string `json:"value"`
		}{jsonId(prop), next}
		out.events.AddAction("x-txt", obj, data)
	}
}

func (out *CommandOutput) StateChange(gobj meta.Instance, prop ident.Id, prev, next ident.Id) {
	if !out.view.InView(gobj) {
		out.Log(fmt.Sprintf("CommandOutput: '%s' not in view, ignoring state change %s(%s->%s)\n", gobj.GetId(), prop, prev, next))
	} else {
		obj := NewObjectRef(gobj)
		data := struct {
			Prop string `json:"prop"`
			Prev string `json:"prev"`
			Next string `json:"next"`
		}{jsonId(prop),
			jsonId(prev),
			jsonId(next)}
		out.events.AddAction("x-set", obj, data)
	}
}

// currently sent on the "one" side of an object for any object value.
// ( ex. actor.whereabouts; not: room.contents. )
func (out *CommandOutput) ReferenceChange(gobj meta.Instance, prop, other ident.Id, prev, next meta.Instance) {
	if out.view.Viewpoint() == gobj {
		var n ident.Id
		if next != nil {
			n = next.GetId()
		}
		out.Log(fmt.Sprintf("CommandOutput: changing view to %s\n", n))
		if out.view.ChangedView(gobj, prop, next) && next != nil {
			out.serial.Include(next)
		}
	} else {
		if out.view.EnteredView(gobj, prop, next) {
			out.serial.Include(gobj)
		}
	}
	relatedView := out.view.InView(gobj) ||
		(prev != nil && out.view.InView(prev)) ||
		(next != nil && out.view.InView(next))

	if !relatedView {
		var p, n ident.Id
		if prev != nil {
			p = prev.GetId()
		}
		if next != nil {
			n = next.GetId()
		}
		out.Log(fmt.Sprintf("CommandOutput: '%s' not in view, ignoring refchange %v(%v->%v)\n", gobj.GetId(), prop, p, n))
	} else {
		obj := NewObjectRef(gobj)

		relChange := struct {
			Prop  string           `json:"prop"`
			Other string           `json:"other"`
			Prev  *resource.Object `json:"prev,omitempty"`
			Next  *resource.Object `json:"next,omitempty"`
		}{Prop: jsonId(prop), Other: jsonId(other)}

		if prev != nil {
			relChange.Prev = NewObjectRef(prev)
		}
		if next != nil {
			relChange.Next = NewObjectRef(next)
		}

		out.events.AddAction("x-rel", obj, relChange)
	}
}

// flushPending buffered lines into the fake display object.
// ( so long as theres a flush before push and pop. )
func (out *CommandOutput) flushPending() {
	if lines := out.text.Flush(); len(lines) > 0 {
		if !EmptyLines(lines) {
			tgt := resource.NewObject("_display_", "_sys_")
			out.events.AddAction("print", tgt, lines)
		}
	}
}

// FXIXI some code -- like report the view --
// prettifies the output by printing blank lines (ugh)
func EmptyLines(lines []string) bool {
	empty := true
	for _, l := range lines {
		if len(l) > 0 {
			empty = false
			break
		}
	}
	return empty
}
<|endoftext|>


<commit_before>package sudoku

import (
	"fmt"
	"log"
	"testing"
)

func TestTechniquesSorted(t *testing.T) {
	lastLikelihood := 0.0
	for i, technique := range AllTechniques {
		if technique.HumanLikelihood() < lastLikelihood {
			t.Fatal("Technique named", technique.Name(), "with index", i, "has a likelihood lower than one of the earlier ones: ", technique.HumanLikelihood(), lastLikelihood)
		}
		lastLikelihood = technique.HumanLikelihood()
	}
}

func TestSubsetIndexes(t *testing.T) {
	result := subsetIndexes(3, 1)
	expectedResult := [][]int{{0}, {1}, {2}}
	subsetIndexHelper(t, result, expectedResult)

	result = subsetIndexes(3, 2)
	expectedResult = [][]int{{0, 1}, {0, 2}, {1, 2}}
	subsetIndexHelper(t, result, expectedResult)

	result = subsetIndexes(5, 3)
	expectedResult = [][]int{{0, 1, 2}, {0, 1, 3}, {0, 1, 4}, {0, 2, 3}, {0, 2, 4}, {0, 3, 4}, {1, 2, 3}, {1, 2, 4}, {1, 3, 4}, {2, 3, 4}}
	subsetIndexHelper(t, result, expectedResult)

	if subsetIndexes(1, 2) != nil {
		t.Log("Subset indexes returned a subset where the length is greater than the len")
		t.Fail()
	}

}

func subsetIndexHelper(t *testing.T, result [][]int, expectedResult [][]int) {
	if len(result) != len(expectedResult) {
		t.Log("subset indexes returned wrong number of results for: ", result, " :", expectedResult)
		t.FailNow()
	}
	for i, item := range result {
		if len(item) != len(expectedResult[0]) {
			t.Log("subset indexes returned a result with wrong numbrer of items ", i, " : ", result, " : ", expectedResult)
			t.FailNow()
		}
		for j, value := range item {
			if value != expectedResult[i][j] {
				t.Log("Subset indexes had wrong number at ", i, ",", j, " : ", result, " : ", expectedResult)
				t.Fail()
			}
		}
	}
}

//multiTestWrapper wraps a testing.T and makes it possible to run loops
//where at least one run through the loop must not Error for the whole test
//to pass. Call t.Reset(), and at any time call Passed() to see if t.Error()
//has been called since last reset.
type loopTest struct {
	t           *testing.T
	lastMessage string
}

func (l loopTest) Reset() {
	l.lastMessage = ""
}

func (l loopTest) Passed() bool {
	return l.lastMessage == ""
}

func (l loopTest) Error(args ...interface{}) {
	l.lastMessage = fmt.Sprint(args...)
}

type solveTechniqueMatchMode int

const (
	solveTechniqueMatchModeAll solveTechniqueMatchMode = iota
	solveTechniqueMatchModeAny
)

type solveTechniqueTestHelperOptions struct {
	transpose bool
	//Whether the descriptions of cells are a list of legal possible individual values, or must all match.
	matchMode    solveTechniqueMatchMode
	targetCells  []cellRef
	pointerCells []cellRef
	targetNums   IntSlice
	pointerNums  IntSlice
	targetSame   cellGroupType
	targetGroup  int
	//If true, will loop over all steps from the technique and see if ANY of them match.
	checkAllSteps bool
	//A way to skip the step generator by provding your own list of steps.
	//Useful if you're going to be do repeated calls to the test helper with the
	//same list of steps.
	stepsToCheck struct {
		grid   *Grid
		solver SolveTechnique
		steps  []*SolveStep
	}
	//If description provided, the description MUST match.
	description string
	//If descriptions provided, ONE of the descriptions must match.
	//generally used in conjunction with solveTechniqueMatchModeAny.
	descriptions []string
	debugPrint   bool
}

//TODO: 97473c18633203a6eaa075d968ba77d85ba28390 introduced an error here where we don't return all techniques,
//at least for forcing chains technique.
func getStepsForTechnique(technique SolveTechnique, grid *Grid, fetchAll bool) []*SolveStep {

	var steps []*SolveStep

	results := make(chan *SolveStep, DIM*DIM)
	done := make(chan bool)

	//Find is meant to be run in a goroutine; it won't complete until it's searched everything.
	go func() {
		technique.Find(grid, results, done)
		//Since we're the only technique running, as soon as this one returns, we can
		//signal up that no more results are coming.
		close(results)
	}()

	for step := range results {
		steps = append(steps, step)
		if !fetchAll {
			//Signal to done if we can.
			select {
			case done <- true:
			default:
			}
			break
		}
	}

	return steps

}

func humanSolveTechniqueTestHelperStepGenerator(t *testing.T, puzzleName string, techniqueName string, options solveTechniqueTestHelperOptions) (*Grid, SolveTechnique, []*SolveStep) {
	grid := NewGrid()
	if !grid.LoadFromFile(puzzlePath(puzzleName)) {
		t.Fatal("Couldn't load puzzle ", puzzleName)
	}

	if options.transpose {
		newGrid := grid.transpose()
		grid.Done()
		grid = newGrid
	}

	solver := techniquesByName[techniqueName]

	if solver == nil {
		t.Fatal("Couldn't find technique object: ", techniqueName)
	}

	steps := getStepsForTechnique(solver, grid, options.checkAllSteps)

	return grid, solver, steps
}

func humanSolveTechniqueTestHelper(t *testing.T, puzzleName string, techniqueName string, options solveTechniqueTestHelperOptions) {
	//TODO: it's weird that you have to pass in puzzleName a second time if you're also passing in options.

	//TODO: test for col and block as well

	var grid *Grid
	var solver SolveTechnique
	var steps []*SolveStep

	if options.stepsToCheck.grid != nil {
		grid = options.stepsToCheck.grid
		solver = options.stepsToCheck.solver
		steps = options.stepsToCheck.steps
	} else {
		grid, solver, steps = humanSolveTechniqueTestHelperStepGenerator(t, puzzleName, techniqueName, options)
	}

	//Check if solveStep is nil here
	if len(steps) == 0 {
		t.Fatal(techniqueName, " didn't find a cell it should have.")
	}

	for _, step := range steps {

		if options.debugPrint {
			log.Println(step)
		}

		if options.matchMode == solveTechniqueMatchModeAll {

			//All must match

			if options.targetCells != nil {
				if !step.TargetCells.sameAsRefs(options.targetCells) {
					t.Error(techniqueName, " had the wrong target cells: ", step.TargetCells)
				}
			}
			if options.pointerCells != nil {
				if !step.PointerCells.sameAsRefs(options.pointerCells) {
					t.Error(techniqueName, " had the wrong pointer cells: ", step.PointerCells)
				}
			}

			switch options.targetSame {
			case _GROUP_ROW:
				if !step.TargetCells.SameRow() || step.TargetCells.Row() != options.targetGroup {
					t.Error("The target cells in the ", techniqueName, " were wrong row :", step.TargetCells.Row())
				}
			case _GROUP_BLOCK:
				if !step.TargetCells.SameBlock() || step.TargetCells.Block() != options.targetGroup {
					t.Error("The target cells in the ", techniqueName, " were wrong block :", step.TargetCells.Block())
				}
			case _GROUP_COL:
				if !step.TargetCells.SameCol() || step.TargetCells.Col() != options.targetGroup {
					t.Error("The target cells in the ", techniqueName, " were wrong col :", step.TargetCells.Col())
				}
			case _GROUP_NONE:
				//Do nothing
			default:
				t.Error("human solve technique helper error: unsupported group type: ", options.targetSame)
			}

			if options.targetNums != nil {
				if !step.TargetNums.SameContentAs(options.targetNums) {
					t.Error(techniqueName, " found the wrong numbers: ", step.TargetNums)
				}
			}

			if options.pointerNums != nil {
				if !step.PointerNums.SameContentAs(options.pointerNums) {
					t.Error(techniqueName, "found the wrong numbers:", step.PointerNums)
				}
			}
		} else if options.matchMode == solveTechniqueMatchModeAny {

			foundMatch := false

			if options.targetCells != nil {
				foundMatch = false
				for _, ref := range options.targetCells {
					for _, cell := range step.TargetCells {
						if ref.Cell(grid) == cell {
							//TODO: break out early
							foundMatch = true
						}
					}
				}
				if !foundMatch {
					t.Error(techniqueName, " had the wrong target cells: ", step.TargetCells)
				}
			}
			if options.pointerCells != nil {
				t.Error("Pointer cells in match mode any not yet supported.")
			}

			if options.targetSame != _GROUP_NONE {
				t.Error("Target Same in match mode any not yet supported.")
			}

			if options.targetNums != nil {
				foundMatch = false
				for _, targetNum := range options.targetNums {
					for _, num := range step.TargetNums {
						if targetNum == num {
							foundMatch = true
							//TODO: break early here.
						}
					}
				}
				if !foundMatch {
					t.Error(techniqueName, " had the wrong target nums: ", step.TargetNums)
				}
			}

			if options.pointerNums != nil {
				foundMatch = false
				for _, pointerNum := range options.pointerNums {
					for _, num := range step.PointerNums {
						if pointerNum == num {
							foundMatch = true
							//TODO: break early here
						}
					}
				}
				if !foundMatch {
					t.Error(techniqueName, " had the wrong pointer nums: ", step.PointerNums)
				}
			}
		}

		if options.description != "" {
			//Normalize the step so that the description will be stable for the test.
			step.normalize()
			description := solver.Description(step)
			if description != options.description {
				t.Error("Wrong description for ", techniqueName, ". Got:*", description, "* expected: *", options.description, "*")
			}
		} else if options.descriptions != nil {
			foundMatch := false
			step.normalize()
			description := solver.Description(step)
			for _, targetDescription := range options.descriptions {
				if description == targetDescription {
					foundMatch = true
				}
			}
			if !foundMatch {
				t.Error("No descriptions matched for ", techniqueName, ". Got:*", description)
			}
		}
	}

	//TODO: we should do exhaustive testing of SolveStep application. We used to test it here, but as long as targetCells and targetNums are correct it should be fine.

	grid.Done()
}
<commit_msg>TESTS FAIL. Added a looping parameter to the loopTest<commit_after>package sudoku

import (
	"fmt"
	"log"
	"testing"
)

func TestTechniquesSorted(t *testing.T) {
	lastLikelihood := 0.0
	for i, technique := range AllTechniques {
		if technique.HumanLikelihood() < lastLikelihood {
			t.Fatal("Technique named", technique.Name(), "with index", i, "has a likelihood lower than one of the earlier ones: ", technique.HumanLikelihood(), lastLikelihood)
		}
		lastLikelihood = technique.HumanLikelihood()
	}
}

func TestSubsetIndexes(t *testing.T) {
	result := subsetIndexes(3, 1)
	expectedResult := [][]int{{0}, {1}, {2}}
	subsetIndexHelper(t, result, expectedResult)

	result = subsetIndexes(3, 2)
	expectedResult = [][]int{{0, 1}, {0, 2}, {1, 2}}
	subsetIndexHelper(t, result, expectedResult)

	result = subsetIndexes(5, 3)
	expectedResult = [][]int{{0, 1, 2}, {0, 1, 3}, {0, 1, 4}, {0, 2, 3}, {0, 2, 4}, {0, 3, 4}, {1, 2, 3}, {1, 2, 4}, {1, 3, 4}, {2, 3, 4}}
	subsetIndexHelper(t, result, expectedResult)

	if subsetIndexes(1, 2) != nil {
		t.Log("Subset indexes returned a subset where the length is greater than the len")
		t.Fail()
	}

}

func subsetIndexHelper(t *testing.T, result [][]int, expectedResult [][]int) {
	if len(result) != len(expectedResult) {
		t.Log("subset indexes returned wrong number of results for: ", result, " :", expectedResult)
		t.FailNow()
	}
	for i, item := range result {
		if len(item) != len(expectedResult[0]) {
			t.Log("subset indexes returned a result with wrong numbrer of items ", i, " : ", result, " : ", expectedResult)
			t.FailNow()
		}
		for j, value := range item {
			if value != expectedResult[i][j] {
				t.Log("Subset indexes had wrong number at ", i, ",", j, " : ", result, " : ", expectedResult)
				t.Fail()
			}
		}
	}
}

//multiTestWrapper wraps a testing.T and makes it possible to run loops
//where at least one run through the loop must not Error for the whole test
//to pass. Call t.Reset(), and at any time call Passed() to see if t.Error()
//has been called since last reset.
//Or, if looping is false, it's just a passthrough to t.Error.
type loopTest struct {
	t           *testing.T
	looping     bool
	lastMessage string
}

func (l loopTest) Reset() {
	l.lastMessage = ""
}

func (l loopTest) Passed() bool {
	return l.lastMessage == ""
}

func (l loopTest) Error(args ...interface{}) {
	if l.looping == false {
		l.t.Error(args...)
	} else {
		l.lastMessage = fmt.Sprint(args...)
	}
}

type solveTechniqueMatchMode int

const (
	solveTechniqueMatchModeAll solveTechniqueMatchMode = iota
	solveTechniqueMatchModeAny
)

type solveTechniqueTestHelperOptions struct {
	transpose bool
	//Whether the descriptions of cells are a list of legal possible individual values, or must all match.
	matchMode    solveTechniqueMatchMode
	targetCells  []cellRef
	pointerCells []cellRef
	targetNums   IntSlice
	pointerNums  IntSlice
	targetSame   cellGroupType
	targetGroup  int
	//If true, will loop over all steps from the technique and see if ANY of them match.
	checkAllSteps bool
	//A way to skip the step generator by provding your own list of steps.
	//Useful if you're going to be do repeated calls to the test helper with the
	//same list of steps.
	stepsToCheck struct {
		grid   *Grid
		solver SolveTechnique
		steps  []*SolveStep
	}
	//If description provided, the description MUST match.
	description string
	//If descriptions provided, ONE of the descriptions must match.
	//generally used in conjunction with solveTechniqueMatchModeAny.
	descriptions []string
	debugPrint   bool
}

//TODO: 97473c18633203a6eaa075d968ba77d85ba28390 introduced an error here where we don't return all techniques,
//at least for forcing chains technique.
func getStepsForTechnique(technique SolveTechnique, grid *Grid, fetchAll bool) []*SolveStep {

	var steps []*SolveStep

	results := make(chan *SolveStep, DIM*DIM)
	done := make(chan bool)

	//Find is meant to be run in a goroutine; it won't complete until it's searched everything.
	go func() {
		technique.Find(grid, results, done)
		//Since we're the only technique running, as soon as this one returns, we can
		//signal up that no more results are coming.
		close(results)
	}()

	for step := range results {
		steps = append(steps, step)
		if !fetchAll {
			//Signal to done if we can.
			select {
			case done <- true:
			default:
			}
			break
		}
	}

	return steps

}

func humanSolveTechniqueTestHelperStepGenerator(t *testing.T, puzzleName string, techniqueName string, options solveTechniqueTestHelperOptions) (*Grid, SolveTechnique, []*SolveStep) {
	grid := NewGrid()
	if !grid.LoadFromFile(puzzlePath(puzzleName)) {
		t.Fatal("Couldn't load puzzle ", puzzleName)
	}

	if options.transpose {
		newGrid := grid.transpose()
		grid.Done()
		grid = newGrid
	}

	solver := techniquesByName[techniqueName]

	if solver == nil {
		t.Fatal("Couldn't find technique object: ", techniqueName)
	}

	steps := getStepsForTechnique(solver, grid, options.checkAllSteps)

	return grid, solver, steps
}

func humanSolveTechniqueTestHelper(t *testing.T, puzzleName string, techniqueName string, options solveTechniqueTestHelperOptions) {
	//TODO: it's weird that you have to pass in puzzleName a second time if you're also passing in options.

	//TODO: test for col and block as well

	var grid *Grid
	var solver SolveTechnique
	var steps []*SolveStep

	if options.stepsToCheck.grid != nil {
		grid = options.stepsToCheck.grid
		solver = options.stepsToCheck.solver
		steps = options.stepsToCheck.steps
	} else {
		grid, solver, steps = humanSolveTechniqueTestHelperStepGenerator(t, puzzleName, techniqueName, options)
	}

	//Check if solveStep is nil here
	if len(steps) == 0 {
		t.Fatal(techniqueName, " didn't find a cell it should have.")
	}

	for _, step := range steps {

		if options.debugPrint {
			log.Println(step)
		}

		if options.matchMode == solveTechniqueMatchModeAll {

			//All must match

			if options.targetCells != nil {
				if !step.TargetCells.sameAsRefs(options.targetCells) {
					t.Error(techniqueName, " had the wrong target cells: ", step.TargetCells)
				}
			}
			if options.pointerCells != nil {
				if !step.PointerCells.sameAsRefs(options.pointerCells) {
					t.Error(techniqueName, " had the wrong pointer cells: ", step.PointerCells)
				}
			}

			switch options.targetSame {
			case _GROUP_ROW:
				if !step.TargetCells.SameRow() || step.TargetCells.Row() != options.targetGroup {
					t.Error("The target cells in the ", techniqueName, " were wrong row :", step.TargetCells.Row())
				}
			case _GROUP_BLOCK:
				if !step.TargetCells.SameBlock() || step.TargetCells.Block() != options.targetGroup {
					t.Error("The target cells in the ", techniqueName, " were wrong block :", step.TargetCells.Block())
				}
			case _GROUP_COL:
				if !step.TargetCells.SameCol() || step.TargetCells.Col() != options.targetGroup {
					t.Error("The target cells in the ", techniqueName, " were wrong col :", step.TargetCells.Col())
				}
			case _GROUP_NONE:
				//Do nothing
			default:
				t.Error("human solve technique helper error: unsupported group type: ", options.targetSame)
			}

			if options.targetNums != nil {
				if !step.TargetNums.SameContentAs(options.targetNums) {
					t.Error(techniqueName, " found the wrong numbers: ", step.TargetNums)
				}
			}

			if options.pointerNums != nil {
				if !step.PointerNums.SameContentAs(options.pointerNums) {
					t.Error(techniqueName, "found the wrong numbers:", step.PointerNums)
				}
			}
		} else if options.matchMode == solveTechniqueMatchModeAny {

			foundMatch := false

			if options.targetCells != nil {
				foundMatch = false
				for _, ref := range options.targetCells {
					for _, cell := range step.TargetCells {
						if ref.Cell(grid) == cell {
							//TODO: break out early
							foundMatch = true
						}
					}
				}
				if !foundMatch {
					t.Error(techniqueName, " had the wrong target cells: ", step.TargetCells)
				}
			}
			if options.pointerCells != nil {
				t.Error("Pointer cells in match mode any not yet supported.")
			}

			if options.targetSame != _GROUP_NONE {
				t.Error("Target Same in match mode any not yet supported.")
			}

			if options.targetNums != nil {
				foundMatch = false
				for _, targetNum := range options.targetNums {
					for _, num := range step.TargetNums {
						if targetNum == num {
							foundMatch = true
							//TODO: break early here.
						}
					}
				}
				if !foundMatch {
					t.Error(techniqueName, " had the wrong target nums: ", step.TargetNums)
				}
			}

			if options.pointerNums != nil {
				foundMatch = false
				for _, pointerNum := range options.pointerNums {
					for _, num := range step.PointerNums {
						if pointerNum == num {
							foundMatch = true
							//TODO: break early here
						}
					}
				}
				if !foundMatch {
					t.Error(techniqueName, " had the wrong pointer nums: ", step.PointerNums)
				}
			}
		}

		if options.description != "" {
			//Normalize the step so that the description will be stable for the test.
			step.normalize()
			description := solver.Description(step)
			if description != options.description {
				t.Error("Wrong description for ", techniqueName, ". Got:*", description, "* expected: *", options.description, "*")
			}
		} else if options.descriptions != nil {
			foundMatch := false
			step.normalize()
			description := solver.Description(step)
			for _, targetDescription := range options.descriptions {
				if description == targetDescription {
					foundMatch = true
				}
			}
			if !foundMatch {
				t.Error("No descriptions matched for ", techniqueName, ". Got:*", description)
			}
		}
	}

	//TODO: we should do exhaustive testing of SolveStep application. We used to test it here, but as long as targetCells and targetNums are correct it should be fine.

	grid.Done()
}
<|endoftext|>


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

package httputils

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

	"github.com/gorilla/websocket"
	eh "github.com/looplab/eventhorizon"
)

// EventBusHandler is a simple event handler for observing events.
type EventBusHandler struct {
	// Use the default options.
	upgrader websocket.Upgrader
	chs      []chan eh.Event
	chsMu    sync.RWMutex
}

// NewEventBusHandler creates a new EventBusHandler.
func NewEventBusHandler() *EventBusHandler {
	return &EventBusHandler{}
}

// HandlerType implements the HandlerType method of the eventhorizon.EventHandler interface.
func (h *EventBusHandler) HandlerType() eh.EventHandlerType {
	return eh.EventHandlerType("websocket")
}

// HandleEvent implements the HandleEvent method of the eventhorizon.EventHandler interface.
func (h *EventBusHandler) HandleEvent(ctx context.Context, event eh.Event) error {
	h.chsMu.RLock()
	defer h.chsMu.RUnlock()

	// Send to all websocket connections.
	for _, ch := range h.chs {
		select {
		case ch <- event:
		default:
			return fmt.Errorf("missed event: %s", event)
		}
	}

	return nil
}

// ServeHTTP implements the ServeHTTP method of the http.Handler interface
// by upgrading requests to websocket connections which will receive all events.
// TODO: Send events as JSON.
func (h *EventBusHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	c, err := h.upgrader.Upgrade(w, r, nil)
	if err != nil {
		log.Printf("eventhorizon: could not upgrade websocket: %s", err)
		return
	}
	defer c.Close()

	ch := make(chan eh.Event, 10)
	h.chsMu.Lock()
	h.chs = append(h.chs, ch)
	h.chsMu.Unlock()

	for event := range ch {
		if err := c.WriteMessage(websocket.TextMessage, []byte(event.String())); err != nil {
			log.Printf("eventhorizon: could not write to websocket: %s", err)
			break
		}
	}
}
<commit_msg>Use EventCodec in HTTP event bus<commit_after>// Copyright (c) 2017 - The Event Horizon authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package httputils

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

	"github.com/gorilla/websocket"

	eh "github.com/looplab/eventhorizon"
	"github.com/looplab/eventhorizon/codec/json"
)

// EventBusHandler is a simple event handler for observing events.
type EventBusHandler struct {
	// Use the default options.
	upgrader websocket.Upgrader
	chs      []chan eh.Event
	chsMu    sync.RWMutex
	codec    eh.EventCodec
}

// NewEventBusHandler creates a new EventBusHandler.
func NewEventBusHandler() *EventBusHandler {
	return &EventBusHandler{
		codec: &json.EventCodec{},
	}
}

// HandlerType implements the HandlerType method of the eventhorizon.EventHandler interface.
func (h *EventBusHandler) HandlerType() eh.EventHandlerType {
	return eh.EventHandlerType("websocket")
}

// HandleEvent implements the HandleEvent method of the eventhorizon.EventHandler interface.
func (h *EventBusHandler) HandleEvent(ctx context.Context, event eh.Event) error {
	h.chsMu.RLock()
	defer h.chsMu.RUnlock()

	// Send to all websocket connections.
	for _, ch := range h.chs {
		select {
		case ch <- event:
		default:
			log.Printf("eventhorizon: publish queue full in websocket event bus: %s", event)
		}
	}

	return nil
}

// ServeHTTP implements the ServeHTTP method of the http.Handler interface
// by upgrading requests to websocket connections which will receive all events.
func (h *EventBusHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	c, err := h.upgrader.Upgrade(w, r, nil)
	if err != nil {
		log.Printf("eventhorizon: could not upgrade websocket: %s", err)
		return
	}
	defer c.Close()

	ch := make(chan eh.Event, 10)
	h.chsMu.Lock()
	h.chs = append(h.chs, ch)
	h.chsMu.Unlock()

	for event := range ch {
		data, err := h.codec.MarshalEvent(context.Background(), event)
		if err != nil {
			log.Printf("eventhorizon: could not marshal websocket event: %s", err)
			break
		}

		if err := c.WriteMessage(websocket.TextMessage, data); err != nil {
			log.Printf("eventhorizon: could not write to websocket: %s", err)
			break
		}
	}
}
<|endoftext|>