mirror of
https://github.com/coder/coder.git
synced 2025-07-12 00:14:10 +00:00
This brings together a bunch of random, partially implemented packages for support of the new(ish) Windows [`conpty`](https://devblogs.microsoft.com/commandline/windows-command-line-introducing-the-windows-pseudo-console-conpty/) API - such that we can leverage the `expect` style of CLI tests, but in a way that works in Linux/OSX `pty`s and Windows `conpty`. These include: - Vendoring the `go-expect` library from Netflix w/ some tweaks to work cross-platform - Vendoring the `pty` cross-platform implementation from [waypoint-plugin-sdk](b55c787a65/internal/pkg/pty
) - Vendoring the `conpty` Windows-specific implementation from [waypoint-plugin-sdk](b55c787a65/internal/pkg/conpty
) - Adjusting the `pty` interface to work with `go-expect` + the cross-plat version There were several limitations with the current packages: - `go-expect` requires the same `os.File` (TTY) for input / output, but `conhost` requires separate file handles - `conpty` does not handle input, only output - The cross-platform `pty` didn't expose the full set of primitives needed for `console` Therefore, the following changes were made: - Handling of `stdin` was added to the `conpty` interface - We weren't using the full extent of the `go-expect` interface, so some portions were removed (ie, exec'ing a process) to simplify our implementation and make it easier to extend cross-platform - Instead of `console` exposing just a `Tty`, it exposes an `InTty` and `OutTty`, to help encapsulate the difference on Windows (on Linux, these point to the same pipe) Future improvements: - The `isatty` implementation doesn't support accurate detection of `conhost` pty's without an associated process. In lieu of a more robust check, I've added a `--force-tty` flag intended for test case use - that forces the CLI to run in tty mode. - It seems the windows implementation doesn't support setting a deadline. This is needed for the expect.Timeout API, but isn't used by us yet. Fixes #241
140 lines
3.4 KiB
Go
140 lines
3.4 KiB
Go
// Copyright 2018 Netflix, Inc.
|
|
//
|
|
// 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 expect
|
|
|
|
import (
|
|
"bytes"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Opt allows settings Expect options.
|
|
type Opt func(*Opts) error
|
|
|
|
// ConsoleCallback is a callback function to execute if a match is found for
|
|
// the chained matcher.
|
|
type ConsoleCallback func(buf *bytes.Buffer) error
|
|
|
|
// Opts provides additional options on Expect.
|
|
type Opts struct {
|
|
Matchers []Matcher
|
|
ReadTimeout *time.Duration
|
|
}
|
|
|
|
// Match sequentially calls Match on all matchers in ExpectOpts and returns the
|
|
// first matcher if a match exists, otherwise nil.
|
|
func (eo Opts) Match(v interface{}) Matcher {
|
|
for _, matcher := range eo.Matchers {
|
|
if matcher.Match(v) {
|
|
return matcher
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CallbackMatcher is a matcher that provides a Callback function.
|
|
type CallbackMatcher interface {
|
|
// Callback executes the matcher's callback with the content buffer at the
|
|
// time of match.
|
|
Callback(buf *bytes.Buffer) error
|
|
}
|
|
|
|
// Matcher provides an interface for finding a match in content read from
|
|
// Console's tty.
|
|
type Matcher interface {
|
|
// Match returns true iff a match is found.
|
|
Match(v interface{}) bool
|
|
Criteria() interface{}
|
|
}
|
|
|
|
// stringMatcher fulfills the Matcher interface to match strings against a given
|
|
// bytes.Buffer.
|
|
type stringMatcher struct {
|
|
str string
|
|
}
|
|
|
|
func (sm *stringMatcher) Match(v interface{}) bool {
|
|
buf, ok := v.(*bytes.Buffer)
|
|
if !ok {
|
|
return false
|
|
}
|
|
if strings.Contains(buf.String(), sm.str) {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (sm *stringMatcher) Criteria() interface{} {
|
|
return sm.str
|
|
}
|
|
|
|
// allMatcher fulfills the Matcher interface to match a group of ExpectOpt
|
|
// against any value.
|
|
type allMatcher struct {
|
|
options Opts
|
|
}
|
|
|
|
func (am *allMatcher) Match(v interface{}) bool {
|
|
var matchers []Matcher
|
|
for _, matcher := range am.options.Matchers {
|
|
if matcher.Match(v) {
|
|
continue
|
|
}
|
|
matchers = append(matchers, matcher)
|
|
}
|
|
|
|
am.options.Matchers = matchers
|
|
return len(matchers) == 0
|
|
}
|
|
|
|
func (am *allMatcher) Criteria() interface{} {
|
|
var criteria []interface{}
|
|
for _, matcher := range am.options.Matchers {
|
|
criteria = append(criteria, matcher.Criteria())
|
|
}
|
|
return criteria
|
|
}
|
|
|
|
// All adds an Expect condition to exit if the content read from Console's tty
|
|
// matches all of the provided ExpectOpt, in any order.
|
|
func All(expectOpts ...Opt) Opt {
|
|
return func(opts *Opts) error {
|
|
var options Opts
|
|
for _, opt := range expectOpts {
|
|
if err := opt(&options); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
opts.Matchers = append(opts.Matchers, &allMatcher{
|
|
options: options,
|
|
})
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// String adds an Expect condition to exit if the content read from Console's
|
|
// tty contains any of the given strings.
|
|
func String(strs ...string) Opt {
|
|
return func(opts *Opts) error {
|
|
for _, str := range strs {
|
|
opts.Matchers = append(opts.Matchers, &stringMatcher{
|
|
str: str,
|
|
})
|
|
}
|
|
return nil
|
|
}
|
|
}
|