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

// Godoc comment extraction and comment -> HTML formatting.

package doc

import (
	"go/ast";
	"io";
	"once";
	"regexp";
	"strings";
	"template";	// for htmlEscape
)

// Comment extraction

var (
	comment_markers *regexp.Regexp;
	trailing_whitespace *regexp.Regexp;
	comment_junk *regexp.Regexp;
)

func makeRex(s string) *regexp.Regexp {
	re, err := regexp.Compile(s);
	if err != nil {
		panic("MakeRegexp ", s, " ", err.String());
	}
	return re;
}

// TODO(rsc): Cannot use var initialization for regexps,
// because Regexp constructor needs threads.
func setupRegexps() {
	comment_markers = makeRex("^/[/*] ?");
	trailing_whitespace = makeRex("[ \t\r]+$");
	comment_junk = makeRex("^[ \t]*(/\\*|\\*/)[ \t]*$");
}

// CommentText returns the text of comment,
// with the comment markers - //, /*, and */ - removed.
func CommentText(comment *ast.CommentGroup) string {
	if comment == nil {
		return "";
	}
	comments := make([]string, len(comment.List));
	for i, c := range comment.List {
		comments[i] = string(c.Text);
	}

	once.Do(setupRegexps);
	lines := make([]string, 0, 20);
	for _, c := range comments {
		// split on newlines
		cl := strings.Split(c, "\n", 0);

		// walk lines, stripping comment markers
		w := 0;
		for _, l := range cl {
			// remove /* and */ lines
			if comment_junk.MatchString(l) {
				continue;
			}

			// strip trailing white space
			m := trailing_whitespace.ExecuteString(l);
			if len(m) > 0 {
				l = l[0 : m[1]];
			}

			// strip leading comment markers
			m = comment_markers.ExecuteString(l);
			if len(m) > 0 {
				l = l[m[1] : len(l)];
			}

			// throw away leading blank lines
			if w == 0 && l == "" {
				continue;
			}

			cl[w] = l;
			w++;
		}

		// throw away trailing blank lines
		for w > 0 && cl[w-1] == "" {
			w--;
		}
		cl = cl[0 : w];

		// add this comment to total list
		// TODO: maybe separate with a single blank line
		// if there is already a comment and len(cl) > 0?
		for _, l := range cl {
			n := len(lines);
			if n+1 >= cap(lines) {
				newlines := make([]string, n, 2*cap(lines));
				for k := range newlines {
					newlines[k] = lines[k];
				}
				lines = newlines;
			}
			lines = lines[0 : n+1];
			lines[n] = l;
		}
	}

	// add final "" entry to get trailing newline.
	// loop always leaves room for one more.
	n := len(lines);
	lines = lines[0 : n+1];

	return strings.Join(lines, "\n");
}

// Split bytes into lines.
func split(text []byte) [][]byte {
	// count lines
	n := 0;
	last := 0;
	for i, c := range text {
		if c == '\n' {
			last = i+1;
			n++;
		}
	}
	if last < len(text) {
		n++;
	}

	// split
	out := make([][]byte, n);
	last = 0;
	n = 0;
	for i, c := range text {
		if c == '\n' {
			out[n] = text[last : i+1];
			last = i+1;
			n++;
		}
	}
	if last < len(text) {
		out[n] = text[last : len(text)];
	}

	return out;
}

var (
	ldquo = strings.Bytes("“");
	rdquo = strings.Bytes("”");
)

// Escape comment text for HTML.
// Also, turn `` into “ and '' into ”.
func commentEscape(w io.Writer, s []byte) {
	last := 0;
	for i := 0; i < len(s)-1; i++ {
		if s[i] == s[i+1] && (s[i] == '`' || s[i] == '\'') {
			template.HtmlEscape(w, s[last : i]);
			last = i+2;
			switch s[i] {
			case '`':
				w.Write(ldquo);
			case '\'':
				w.Write(rdquo);
			}
			i++;	// loop will add one more
		}
	}
	template.HtmlEscape(w, s[last : len(s)]);
}

var (
	html_p = strings.Bytes("
\n");
	html_endp = strings.Bytes("
\n");
	html_pre = strings.Bytes("
");
	html_endpre = strings.Bytes("
\n");
)

func indentLen(s []byte) int {
	i := 0;
	for i < len(s) && (s[i] == ' ' || s[i] == '\t') {
		i++;
	}
	return i;
}

func isBlank(s []byte) bool {
	return len(s) == 0 || (len(s) == 1 && s[0] == '\n')
}

func commonPrefix(a, b []byte) []byte {
	i := 0;
	for i < len(a) && i < len(b) && a[i] == b[i] {
		i++;
	}
	return a[0 : i];
}

func unindent(block [][]byte) {
	if len(block) == 0 {
		return;
	}

	// compute maximum common white prefix
	prefix := block[0][0 : indentLen(block[0])];
	for _, line := range block {
		if !isBlank(line) {
			prefix = commonPrefix(prefix, line[0 : indentLen(line)]);
		}
	}
	n := len(prefix);

	// remove
	for i, line := range block {
		if !isBlank(line) {
			block[i] = line[n : len(line)];
		}
	}
}

// Convert comment text to formatted HTML.
// The comment was prepared by DocReader,
// so it is known not to have leading, trailing blank lines
// nor to have trailing spaces at the end of lines.
// The comment markers have already been removed.
//
// Turn each run of multiple \n into

// Turn each run of indented lines into
 without indent.
//
// TODO(rsc): I'd like to pass in an array of variable names []string
// and then italicize those strings when they appear as words.
func ToHtml(w io.Writer, s []byte) {
	inpara := false;

	/* TODO(rsc): 6g cant generate code for these
	close := func() {
		if inpara {
			w.Write(html_endp);
			inpara = false;
		}
	};
	open := func() {
		if !inpara {
			w.Write(html_p);
			inpara = true;
		}
	};
	*/

	lines := split(s);
	unindent(lines);
	for i := 0; i < len(lines);  {
		line := lines[i];
		if isBlank(line) {
			// close paragraph
			if inpara {
				w.Write(html_endp);
				inpara = false;
			}
			i++;
			continue;
		}
		if indentLen(line) > 0 {
			// close paragraph
			if inpara {
				w.Write(html_endp);
				inpara = false;
			}

			// count indented or blank lines
			j := i+1;
			for j < len(lines) && (isBlank(lines[j]) || indentLen(lines[j]) > 0) {
				j++;
			}
			// but not trailing blank lines
			for j > i && isBlank(lines[j-1]) {
				j--;
			}
			block := lines[i : j];
			i = j;

			unindent(block);

			// put those lines in a pre block.
			// they don't get the nice text formatting,
			// just html escaping
			w.Write(html_pre);
			for _, line := range block {
				template.HtmlEscape(w, line);
			}
			w.Write(html_endpre);
			continue;
		}
		// open paragraph
		if !inpara {
			w.Write(html_p);
			inpara = true;
		}
		commentEscape(w, lines[i]);
		i++;
	}
	if inpara {
		w.Write(html_endp);
		inpara = false;
	}
}

<commit_msg>preserve blank lines in // comments<commit_after>// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Godoc comment extraction and comment -> HTML formatting.

package doc

import (
	"go/ast";
	"io";
	"once";
	"regexp";
	"strings";
	"template";	// for htmlEscape
)

// Comment extraction

var (
	comment_markers *regexp.Regexp;
	trailing_whitespace *regexp.Regexp;
	comment_junk *regexp.Regexp;
)

func makeRex(s string) *regexp.Regexp {
	re, err := regexp.Compile(s);
	if err != nil {
		panic("MakeRegexp ", s, " ", err.String());
	}
	return re;
}

// TODO(rsc): Cannot use var initialization for regexps,
// because Regexp constructor needs threads.
func setupRegexps() {
	comment_markers = makeRex("^/[/*] ?");
	trailing_whitespace = makeRex("[ \t\r]+$");
	comment_junk = makeRex("^[ \t]*(/\\*|\\*/)[ \t]*$");
}

// CommentText returns the text of comment,
// with the comment markers - //, /*, and */ - removed.
func CommentText(comment *ast.CommentGroup) string {
	if comment == nil {
		return "";
	}
	comments := make([]string, len(comment.List));
	for i, c := range comment.List {
		comments[i] = string(c.Text);
	}

	once.Do(setupRegexps);
	lines := make([]string, 0, 20);
	for _, c := range comments {
		// split on newlines
		cl := strings.Split(c, "\n", 0);

		// walk lines, stripping comment markers
		w := 0;
		for _, l := range cl {
			// remove /* and */ lines
			if comment_junk.MatchString(l) {
				continue;
			}

			// strip trailing white space
			m := trailing_whitespace.ExecuteString(l);
			if len(m) > 0 {
				l = l[0 : m[1]];
			}

			// strip leading comment markers
			m = comment_markers.ExecuteString(l);
			if len(m) > 0 {
				l = l[m[1] : len(l)];
			}

			cl[w] = l;
			w++;
		}
		cl = cl[0:w];

		// Add this comment to total list.
		for _, l := range cl {
			n := len(lines);
			if n+1 >= cap(lines) {
				newlines := make([]string, n, 2*cap(lines));
				for k := range newlines {
					newlines[k] = lines[k];
				}
				lines = newlines;
			}
			lines = lines[0 : n+1];
			lines[n] = l;
		}
	}

	// Remove leading blank lines; convert runs of
	// interior blank lines to a single blank line.
	n := 0;
	for _, line := range lines {
		if line != "" || n > 0 && lines[n-1] != "" {
			lines[n] = line;
			n++;
		}
	}
	lines = lines[0 : n];

	// Add final "" entry to get trailing newline from Join.
	// The original loop always leaves room for one more.
	if n > 0 && lines[n-1] != "" {
		lines = lines[0 : n+1];
		lines[n] = "";
	}

	return strings.Join(lines, "\n");
}

// Split bytes into lines.
func split(text []byte) [][]byte {
	// count lines
	n := 0;
	last := 0;
	for i, c := range text {
		if c == '\n' {
			last = i+1;
			n++;
		}
	}
	if last < len(text) {
		n++;
	}

	// split
	out := make([][]byte, n);
	last = 0;
	n = 0;
	for i, c := range text {
		if c == '\n' {
			out[n] = text[last : i+1];
			last = i+1;
			n++;
		}
	}
	if last < len(text) {
		out[n] = text[last : len(text)];
	}

	return out;
}

var (
	ldquo = strings.Bytes("“");
	rdquo = strings.Bytes("”");
)

// Escape comment text for HTML.
// Also, turn `` into “ and '' into ”.
func commentEscape(w io.Writer, s []byte) {
	last := 0;
	for i := 0; i < len(s)-1; i++ {
		if s[i] == s[i+1] && (s[i] == '`' || s[i] == '\'') {
			template.HtmlEscape(w, s[last : i]);
			last = i+2;
			switch s[i] {
			case '`':
				w.Write(ldquo);
			case '\'':
				w.Write(rdquo);
			}
			i++;	// loop will add one more
		}
	}
	template.HtmlEscape(w, s[last : len(s)]);
}

var (
	html_p = strings.Bytes("
\n");
	html_endp = strings.Bytes("
\n");
	html_pre = strings.Bytes("
");
	html_endpre = strings.Bytes("
\n");
)

func indentLen(s []byte) int {
	i := 0;
	for i < len(s) && (s[i] == ' ' || s[i] == '\t') {
		i++;
	}
	return i;
}

func isBlank(s []byte) bool {
	return len(s) == 0 || (len(s) == 1 && s[0] == '\n')
}

func commonPrefix(a, b []byte) []byte {
	i := 0;
	for i < len(a) && i < len(b) && a[i] == b[i] {
		i++;
	}
	return a[0 : i];
}

func unindent(block [][]byte) {
	if len(block) == 0 {
		return;
	}

	// compute maximum common white prefix
	prefix := block[0][0 : indentLen(block[0])];
	for _, line := range block {
		if !isBlank(line) {
			prefix = commonPrefix(prefix, line[0 : indentLen(line)]);
		}
	}
	n := len(prefix);

	// remove
	for i, line := range block {
		if !isBlank(line) {
			block[i] = line[n : len(line)];
		}
	}
}

// Convert comment text to formatted HTML.
// The comment was prepared by DocReader,
// so it is known not to have leading, trailing blank lines
// nor to have trailing spaces at the end of lines.
// The comment markers have already been removed.
//
// Turn each run of multiple \n into

// Turn each run of indented lines into
 without indent.
//
// TODO(rsc): I'd like to pass in an array of variable names []string
// and then italicize those strings when they appear as words.
func ToHtml(w io.Writer, s []byte) {
	inpara := false;

	/* TODO(rsc): 6g cant generate code for these
	close := func() {
		if inpara {
			w.Write(html_endp);
			inpara = false;
		}
	};
	open := func() {
		if !inpara {
			w.Write(html_p);
			inpara = true;
		}
	};
	*/

	lines := split(s);
	unindent(lines);
	for i := 0; i < len(lines);  {
		line := lines[i];
		if isBlank(line) {
			// close paragraph
			if inpara {
				w.Write(html_endp);
				inpara = false;
			}
			i++;
			continue;
		}
		if indentLen(line) > 0 {
			// close paragraph
			if inpara {
				w.Write(html_endp);
				inpara = false;
			}

			// count indented or blank lines
			j := i+1;
			for j < len(lines) && (isBlank(lines[j]) || indentLen(lines[j]) > 0) {
				j++;
			}
			// but not trailing blank lines
			for j > i && isBlank(lines[j-1]) {
				j--;
			}
			block := lines[i : j];
			i = j;

			unindent(block);

			// put those lines in a pre block.
			// they don't get the nice text formatting,
			// just html escaping
			w.Write(html_pre);
			for _, line := range block {
				template.HtmlEscape(w, line);
			}
			w.Write(html_endpre);
			continue;
		}
		// open paragraph
		if !inpara {
			w.Write(html_p);
			inpara = true;
		}
		commentEscape(w, lines[i]);
		i++;
	}
	if inpara {
		w.Write(html_endp);
		inpara = false;
	}
}

<|endoftext|>


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

// Godoc comment extraction and comment -> HTML formatting.

package doc

import (
	"go/ast"
	"http" // for URLEscape
	"io"
	"regexp"
	"strings"
	"template" // for htmlEscape
)

func isWhitespace(ch byte) bool { return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' }

func stripTrailingWhitespace(s string) string {
	i := len(s)
	for i > 0 && isWhitespace(s[i-1]) {
		i--
	}
	return s[0:i]
}

// CommentText returns the text of comment,
// with the comment markers - //, /*, and */ - removed.
func CommentText(comment *ast.CommentGroup) string {
	if comment == nil {
		return ""
	}
	comments := make([]string, len(comment.List))
	for i, c := range comment.List {
		comments[i] = string(c.Text)
	}

	lines := make([]string, 0, 10) // most comments are less than 10 lines
	for _, c := range comments {
		// Remove comment markers.
		// The parser has given us exactly the comment text.
		switch c[1] {
		case '/':
			//-style comment
			c = c[2:]
			// Remove leading space after //, if there is one.
			// TODO(gri) This appears to be necessary in isolated
			//           cases (bignum.RatFromString) - why?
			if len(c) > 0 && c[0] == ' ' {
				c = c[1:]
			}
		case '*':
			/*-style comment */
			c = c[2 : len(c)-2]
		}

		// Split on newlines.
		cl := strings.Split(c, "\n", -1)

		// Walk lines, stripping trailing white space and adding to list.
		for _, l := range cl {
			l = stripTrailingWhitespace(l)
			// Add to list.
			n := len(lines)
			if n+1 >= cap(lines) {
				newlines := make([]string, n, 2*cap(lines))
				copy(newlines, lines)
				lines = newlines
			}
			lines = lines[0 : n+1]
			lines[n] = l
		}
	}

	// Remove leading blank lines; convert runs of
	// interior blank lines to a single blank line.
	n := 0
	for _, line := range lines {
		if line != "" || n > 0 && lines[n-1] != "" {
			lines[n] = line
			n++
		}
	}
	lines = lines[0:n]

	// Add final "" entry to get trailing newline from Join.
	// The original loop always leaves room for one more.
	if n > 0 && lines[n-1] != "" {
		lines = lines[0 : n+1]
		lines[n] = ""
	}

	return strings.Join(lines, "\n")
}

// Split bytes into lines.
func split(text []byte) [][]byte {
	// count lines
	n := 0
	last := 0
	for i, c := range text {
		if c == '\n' {
			last = i + 1
			n++
		}
	}
	if last < len(text) {
		n++
	}

	// split
	out := make([][]byte, n)
	last = 0
	n = 0
	for i, c := range text {
		if c == '\n' {
			out[n] = text[last : i+1]
			last = i + 1
			n++
		}
	}
	if last < len(text) {
		out[n] = text[last:]
	}

	return out
}

var (
	ldquo = []byte("“")
	rdquo = []byte("”")
)

// Escape comment text for HTML. If nice is set,
// also turn `` into “ and '' into ”.
func commentEscape(w io.Writer, s []byte, nice bool) {
	last := 0
	if nice {
		for i := 0; i < len(s)-1; i++ {
			ch := s[i]
			if ch == s[i+1] && (ch == '`' || ch == '\'') {
				template.HTMLEscape(w, s[last:i])
				last = i + 2
				switch ch {
				case '`':
					w.Write(ldquo)
				case '\'':
					w.Write(rdquo)
				}
				i++ // loop will add one more
			}
		}
	}
	template.HTMLEscape(w, s[last:])
}

const (
	// Regexp for Go identifiers
	identRx = `[a-zA-Z_][a-zA-Z_0-9]*` // TODO(gri) ASCII only for now - fix this

	// Regexp for URLs
	protocol = `(https?|ftp|file|gopher|mailto|news|nntp|telnet|wais|prospero):`
	hostPart = `[a-zA-Z0-9_@\-]+`
	filePart = `[a-zA-Z0-9_?%#~&/\-+=]+`
	urlRx    = protocol + `//` + // http://
		hostPart + `([.:]` + hostPart + `)*/?` + // //www.google.com:8080/
		filePart + `([:.,]` + filePart + `)*`
)

var matchRx = regexp.MustCompile(`(` + identRx + `)|(` + urlRx + `)`)

var (
	html_a      = []byte(`<a href="`)
	html_aq     = []byte(`">`)
	html_enda   = []byte("")
	html_i      = []byte("")
	html_endi   = []byte("")
	html_p      = []byte("
\n")
	html_endp   = []byte("
\n")
	html_pre    = []byte("
")
	html_endpre = []byte("
\n")
)

// Emphasize and escape a line of text for HTML. URLs are converted into links;
// if the URL also appears in the words map, the link is taken from the map (if
// the corresponding map value is the empty string, the URL is not converted
// into a link). Go identifiers that appear in the words map are italicized; if
// the corresponding map value is not the empty string, it is considered a URL
// and the word is converted into a link. If nice is set, the remaining text's
// appearance is improved where is makes sense (e.g., `` is turned into “
// and '' into ”).
func emphasize(w io.Writer, line []byte, words map[string]string, nice bool) {
	for {
		m := matchRx.FindSubmatchIndex(line)
		if m == nil {
			break
		}
		// m >= 6 (two parenthesized sub-regexps in matchRx, 1st one is identRx)

		// write text before match
		commentEscape(w, line[0:m[0]], nice)

		// analyze match
		match := line[m[0]:m[1]]
		url := ""
		italics := false
		if words != nil {
			url, italics = words[string(match)]
		}
		if m[2] < 0 {
			// didn't match against first parenthesized sub-regexp; must be match against urlRx
			if !italics {
				// no alternative URL in words list, use match instead
				url = string(match)
			}
			italics = false // don't italicize URLs
		}

		// write match
		if len(url) > 0 {
			w.Write(html_a)
			w.Write([]byte(http.URLEscape(url)))
			w.Write(html_aq)
		}
		if italics {
			w.Write(html_i)
		}
		commentEscape(w, match, nice)
		if italics {
			w.Write(html_endi)
		}
		if len(url) > 0 {
			w.Write(html_enda)
		}

		// advance
		line = line[m[1]:]
	}
	commentEscape(w, line, nice)
}

func indentLen(s []byte) int {
	i := 0
	for i < len(s) && (s[i] == ' ' || s[i] == '\t') {
		i++
	}
	return i
}

func isBlank(s []byte) bool { return len(s) == 0 || (len(s) == 1 && s[0] == '\n') }

func commonPrefix(a, b []byte) []byte {
	i := 0
	for i < len(a) && i < len(b) && a[i] == b[i] {
		i++
	}
	return a[0:i]
}

func unindent(block [][]byte) {
	if len(block) == 0 {
		return
	}

	// compute maximum common white prefix
	prefix := block[0][0:indentLen(block[0])]
	for _, line := range block {
		if !isBlank(line) {
			prefix = commonPrefix(prefix, line[0:indentLen(line)])
		}
	}
	n := len(prefix)

	// remove
	for i, line := range block {
		if !isBlank(line) {
			block[i] = line[n:]
		}
	}
}

// Convert comment text to formatted HTML.
// The comment was prepared by DocReader,
// so it is known not to have leading, trailing blank lines
// nor to have trailing spaces at the end of lines.
// The comment markers have already been removed.
//
// Turn each run of multiple \n into

// Turn each run of indented lines into a
 block without indent.
//
// URLs in the comment text are converted into links; if the URL also appears
// in the words map, the link is taken from the map (if the corresponding map
// value is the empty string, the URL is not converted into a link).
//
// Go identifiers that appear in the words map are italicized; if the corresponding
// map value is not the empty string, it is considered a URL and the word is converted
// into a link.
func ToHTML(w io.Writer, s []byte, words map[string]string) {
	inpara := false

	close := func() {
		if inpara {
			w.Write(html_endp)
			inpara = false
		}
	}
	open := func() {
		if !inpara {
			w.Write(html_p)
			inpara = true
		}
	}

	lines := split(s)
	unindent(lines)
	for i := 0; i < len(lines); {
		line := lines[i]
		if isBlank(line) {
			// close paragraph
			close()
			i++
			continue
		}
		if indentLen(line) > 0 {
			// close paragraph
			close()

			// count indented or blank lines
			j := i + 1
			for j < len(lines) && (isBlank(lines[j]) || indentLen(lines[j]) > 0) {
				j++
			}
			// but not trailing blank lines
			for j > i && isBlank(lines[j-1]) {
				j--
			}
			block := lines[i:j]
			i = j

			unindent(block)

			// put those lines in a pre block
			w.Write(html_pre)
			for _, line := range block {
				emphasize(w, line, nil, false) // no nice text formatting
			}
			w.Write(html_endpre)
			continue
		}
		// open paragraph
		open()
		emphasize(w, lines[i], words, true) // nice text formatting
		i++
	}
	close()
}
<commit_msg>go/doc: use correct escaper for URL<commit_after>// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Godoc comment extraction and comment -> HTML formatting.

package doc

import (
	"go/ast"
	"io"
	"regexp"
	"strings"
	"template" // for htmlEscape
)

func isWhitespace(ch byte) bool { return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' }

func stripTrailingWhitespace(s string) string {
	i := len(s)
	for i > 0 && isWhitespace(s[i-1]) {
		i--
	}
	return s[0:i]
}

// CommentText returns the text of comment,
// with the comment markers - //, /*, and */ - removed.
func CommentText(comment *ast.CommentGroup) string {
	if comment == nil {
		return ""
	}
	comments := make([]string, len(comment.List))
	for i, c := range comment.List {
		comments[i] = string(c.Text)
	}

	lines := make([]string, 0, 10) // most comments are less than 10 lines
	for _, c := range comments {
		// Remove comment markers.
		// The parser has given us exactly the comment text.
		switch c[1] {
		case '/':
			//-style comment
			c = c[2:]
			// Remove leading space after //, if there is one.
			// TODO(gri) This appears to be necessary in isolated
			//           cases (bignum.RatFromString) - why?
			if len(c) > 0 && c[0] == ' ' {
				c = c[1:]
			}
		case '*':
			/*-style comment */
			c = c[2 : len(c)-2]
		}

		// Split on newlines.
		cl := strings.Split(c, "\n", -1)

		// Walk lines, stripping trailing white space and adding to list.
		for _, l := range cl {
			l = stripTrailingWhitespace(l)
			// Add to list.
			n := len(lines)
			if n+1 >= cap(lines) {
				newlines := make([]string, n, 2*cap(lines))
				copy(newlines, lines)
				lines = newlines
			}
			lines = lines[0 : n+1]
			lines[n] = l
		}
	}

	// Remove leading blank lines; convert runs of
	// interior blank lines to a single blank line.
	n := 0
	for _, line := range lines {
		if line != "" || n > 0 && lines[n-1] != "" {
			lines[n] = line
			n++
		}
	}
	lines = lines[0:n]

	// Add final "" entry to get trailing newline from Join.
	// The original loop always leaves room for one more.
	if n > 0 && lines[n-1] != "" {
		lines = lines[0 : n+1]
		lines[n] = ""
	}

	return strings.Join(lines, "\n")
}

// Split bytes into lines.
func split(text []byte) [][]byte {
	// count lines
	n := 0
	last := 0
	for i, c := range text {
		if c == '\n' {
			last = i + 1
			n++
		}
	}
	if last < len(text) {
		n++
	}

	// split
	out := make([][]byte, n)
	last = 0
	n = 0
	for i, c := range text {
		if c == '\n' {
			out[n] = text[last : i+1]
			last = i + 1
			n++
		}
	}
	if last < len(text) {
		out[n] = text[last:]
	}

	return out
}

var (
	ldquo = []byte("“")
	rdquo = []byte("”")
)

// Escape comment text for HTML. If nice is set,
// also turn `` into “ and '' into ”.
func commentEscape(w io.Writer, s []byte, nice bool) {
	last := 0
	if nice {
		for i := 0; i < len(s)-1; i++ {
			ch := s[i]
			if ch == s[i+1] && (ch == '`' || ch == '\'') {
				template.HTMLEscape(w, s[last:i])
				last = i + 2
				switch ch {
				case '`':
					w.Write(ldquo)
				case '\'':
					w.Write(rdquo)
				}
				i++ // loop will add one more
			}
		}
	}
	template.HTMLEscape(w, s[last:])
}

const (
	// Regexp for Go identifiers
	identRx = `[a-zA-Z_][a-zA-Z_0-9]*` // TODO(gri) ASCII only for now - fix this

	// Regexp for URLs
	protocol = `(https?|ftp|file|gopher|mailto|news|nntp|telnet|wais|prospero):`
	hostPart = `[a-zA-Z0-9_@\-]+`
	filePart = `[a-zA-Z0-9_?%#~&/\-+=]+`
	urlRx    = protocol + `//` + // http://
		hostPart + `([.:]` + hostPart + `)*/?` + // //www.google.com:8080/
		filePart + `([:.,]` + filePart + `)*`
)

var matchRx = regexp.MustCompile(`(` + identRx + `)|(` + urlRx + `)`)

var (
	html_a      = []byte(`<a href="`)
	html_aq     = []byte(`">`)
	html_enda   = []byte("")
	html_i      = []byte("")
	html_endi   = []byte("")
	html_p      = []byte("
\n")
	html_endp   = []byte("
\n")
	html_pre    = []byte("
")
	html_endpre = []byte("
\n")
)

// Emphasize and escape a line of text for HTML. URLs are converted into links;
// if the URL also appears in the words map, the link is taken from the map (if
// the corresponding map value is the empty string, the URL is not converted
// into a link). Go identifiers that appear in the words map are italicized; if
// the corresponding map value is not the empty string, it is considered a URL
// and the word is converted into a link. If nice is set, the remaining text's
// appearance is improved where is makes sense (e.g., `` is turned into “
// and '' into ”).
func emphasize(w io.Writer, line []byte, words map[string]string, nice bool) {
	for {
		m := matchRx.FindSubmatchIndex(line)
		if m == nil {
			break
		}
		// m >= 6 (two parenthesized sub-regexps in matchRx, 1st one is identRx)

		// write text before match
		commentEscape(w, line[0:m[0]], nice)

		// analyze match
		match := line[m[0]:m[1]]
		url := ""
		italics := false
		if words != nil {
			url, italics = words[string(match)]
		}
		if m[2] < 0 {
			// didn't match against first parenthesized sub-regexp; must be match against urlRx
			if !italics {
				// no alternative URL in words list, use match instead
				url = string(match)
			}
			italics = false // don't italicize URLs
		}

		// write match
		if len(url) > 0 {
			w.Write(html_a)
			template.HTMLEscape(w, []byte(url))
			w.Write(html_aq)
		}
		if italics {
			w.Write(html_i)
		}
		commentEscape(w, match, nice)
		if italics {
			w.Write(html_endi)
		}
		if len(url) > 0 {
			w.Write(html_enda)
		}

		// advance
		line = line[m[1]:]
	}
	commentEscape(w, line, nice)
}

func indentLen(s []byte) int {
	i := 0
	for i < len(s) && (s[i] == ' ' || s[i] == '\t') {
		i++
	}
	return i
}

func isBlank(s []byte) bool { return len(s) == 0 || (len(s) == 1 && s[0] == '\n') }

func commonPrefix(a, b []byte) []byte {
	i := 0
	for i < len(a) && i < len(b) && a[i] == b[i] {
		i++
	}
	return a[0:i]
}

func unindent(block [][]byte) {
	if len(block) == 0 {
		return
	}

	// compute maximum common white prefix
	prefix := block[0][0:indentLen(block[0])]
	for _, line := range block {
		if !isBlank(line) {
			prefix = commonPrefix(prefix, line[0:indentLen(line)])
		}
	}
	n := len(prefix)

	// remove
	for i, line := range block {
		if !isBlank(line) {
			block[i] = line[n:]
		}
	}
}

// Convert comment text to formatted HTML.
// The comment was prepared by DocReader,
// so it is known not to have leading, trailing blank lines
// nor to have trailing spaces at the end of lines.
// The comment markers have already been removed.
//
// Turn each run of multiple \n into

// Turn each run of indented lines into a
 block without indent.
//
// URLs in the comment text are converted into links; if the URL also appears
// in the words map, the link is taken from the map (if the corresponding map
// value is the empty string, the URL is not converted into a link).
//
// Go identifiers that appear in the words map are italicized; if the corresponding
// map value is not the empty string, it is considered a URL and the word is converted
// into a link.
func ToHTML(w io.Writer, s []byte, words map[string]string) {
	inpara := false

	close := func() {
		if inpara {
			w.Write(html_endp)
			inpara = false
		}
	}
	open := func() {
		if !inpara {
			w.Write(html_p)
			inpara = true
		}
	}

	lines := split(s)
	unindent(lines)
	for i := 0; i < len(lines); {
		line := lines[i]
		if isBlank(line) {
			// close paragraph
			close()
			i++
			continue
		}
		if indentLen(line) > 0 {
			// close paragraph
			close()

			// count indented or blank lines
			j := i + 1
			for j < len(lines) && (isBlank(lines[j]) || indentLen(lines[j]) > 0) {
				j++
			}
			// but not trailing blank lines
			for j > i && isBlank(lines[j-1]) {
				j--
			}
			block := lines[i:j]
			i = j

			unindent(block)

			// put those lines in a pre block
			w.Write(html_pre)
			for _, line := range block {
				emphasize(w, line, nil, false) // no nice text formatting
			}
			w.Write(html_endpre)
			continue
		}
		// open paragraph
		open()
		emphasize(w, lines[i], words, true) // nice text formatting
		i++
	}
	close()
}
<|endoftext|>


<commit_before>package context

import (
	"bytes"
	"fmt"
	"image/color"
	"io"
	"io/ioutil"
	"log"
	"os"
	"path/filepath"
	"reflect"
	"regexp"
	"testing"

	"github.com/drewwells/spritewell"
)

func cleanUpSprites(sprites map[string]spritewell.ImageList) {
	if sprites == nil {
		return
	}
	for _, iml := range sprites {
		err := os.Remove(filepath.Join(iml.GenImgDir, iml.OutFile))
		if err != nil {
			log.Fatal(err)
		}
	}
}

func fileString(path string) string {
	bytes, err := ioutil.ReadFile(path)
	if err != nil {
		panic(err)
	}
	return string(bytes)
}

func fileReader(path string) io.Reader {
	reader, err := os.Open(path)
	if err != nil {
		panic(err)
	}
	return reader
}

var rerandom *regexp.Regexp

func init() {
	// Setup build directory
	os.MkdirAll("test/build", 0755)
	rerandom = regexp.MustCompile(`-\w{6}(?:\.(png|jpg))`)

}

func TestContextFile(t *testing.T) {

	in := bytes.NewBufferString(`div {
  span {
    color: black;
  }
  width: 100px;
  height: 100px;
}

p {
	background: red;
}`)

	var out bytes.Buffer
	ctx := Context{}
	err := ctx.Compile(in, &out)
	if err != nil {
		panic(err)
	}

	e := `div {
  width: 100px;
  height: 100px; }
  div span {
    color: black; }

p {
  background: red; }
`

	if e != out.String() {
		t.Errorf("wanted:\n%s\n"+
			"got:\n%s\n", e, out.String())
	}
}

func TestContextNilRun(t *testing.T) {
	var in, out bytes.Buffer
	ctx := Context{}
	err := ctx.Compile(&in, &out)
	if err == nil {
		t.Error("No error returned")
	}
	if e := "No input provided"; e != err.Error() {
		t.Errorf("wanted:\n%s\ngot:\n%s", e, err)
	}
}

func TestContextRun(t *testing.T) {

	in := bytes.NewBufferString(`$red-var: red;
$hex: #00FF00;
div {
  background: $hex;
  $hex: #00DD00;
  font-size: 10pt;
}
`)

	var out bytes.Buffer
	ctx := Context{}
	err := ctx.Compile(in, &out)
	if err != nil {
		panic(err)
	}

	e := `div {
  background: #00FF00;
  font-size: 10pt; }
`

	if e != out.String() {
		t.Errorf("wanted:\n%s\n"+
			"got:\n%s\n", e, out.String())
	}

}

func TestContextCustomSimpleTypes(t *testing.T) {
	in := bytes.NewBufferString(`div {
  background: foo(null, 3, asdf, false, #005500);
}`)

	//var out bytes.Buffer

	ctx := Context{}
	ctx.Cookies = make([]Cookie, 1)
	// Communication channel for the C Sass callback function
	ch := make(chan []interface{}, 1)
	ctx.Cookies[0] = Cookie{
		"foo($null, $num, $str, $bool, $color)", func(c *Context, usv UnionSassValue) UnionSassValue {
			// Send the interface fn arguments to the ch channel

			var n interface{}
			var num float64
			var s string
			var b bool
			var col = color.RGBA{}
			var intf = []interface{}{n, num, s, b, col}
			Unmarshal(usv, &intf)
			ch <- intf
			res, _ := Marshal(false)
			return res
		}, &ctx,
	}
	var out bytes.Buffer
	err := ctx.Compile(in, &out)
	if err != nil {
		t.Error(err)
	}

	e := []interface{}{
		"<nil>",
		3.0,
		"asdf",
		false,
		color.RGBA{R: 0x0, G: 0x55, B: 0x0, A: 0x1},
	}
	args := <-ch
	if !reflect.DeepEqual(e, args) {
		t.Errorf("wanted:\n% #v\ngot:\n% #v", e, args)
	}
}

func TestContextCustomComplexTypes(t *testing.T) {
	in := bytes.NewBufferString(`div {
	     background: foo((a,b,1,#003300));
	   }`)

	var out bytes.Buffer
	ctx := Context{}
	if ctx.Cookies == nil {
		ctx.Cookies = make([]Cookie, 1)
	}
	ch := make(chan interface{}, 1)
	ctx.Cookies[0] = Cookie{
		"foo($list)", func(c *Context, usv UnionSassValue) UnionSassValue {
			var sv interface{}
			Unmarshal(usv, &sv)
			ch <- sv
			res, _ := Marshal(false)
			return res
		}, &ctx,
	}
	err := ctx.Compile(in, &out)
	if err != nil {
		t.Error(err)
	}

	e := []interface{}{
		"a",
		"b",
		float64(1),
		color.RGBA{R: 0x0, G: 0x33, B: 0x0, A: 0x1},
	}
	args := <-ch
	if !reflect.DeepEqual(e, args) {
		t.Errorf("wanted:\n%#v\ngot:\n% #v", e, args)
	}
}

func TestContextCustomArity(t *testing.T) {
	in := bytes.NewBufferString(`div {
  color: red(blue);
  background: foo(1, 2);
}`)

	var out bytes.Buffer
	ctx := Context{}
	if ctx.Cookies == nil {
		ctx.Cookies = make([]Cookie, 1)
	}

	ctx.Cookies[0] = Cookie{
		"foo()", SampleCB, &ctx,
	}
	err := ctx.Compile(in, &out)
	if err == nil {
		t.Skip("No error thrown for incorrect arity")
	}

	if e := "function foo only takes 0 arguments; given 2"; e != ctx.Errors.Message {
		t.Skipf("wanted:\n%s\ngot:\n%s\n", e, ctx.Errors.Message)
	}
}

func ExampleContext_Compile() {
	in := bytes.NewBufferString(`div {
			  color: red(blue);
			  background: foo();
			}`)

	var out bytes.Buffer
	ctx := Context{
	//Customs: []string{"foo()"},
	}
	if ctx.Cookies == nil {
		ctx.Cookies = make([]Cookie, 1)
	}
	ctx.Cookies[0] = Cookie{
		"foo()", func(c *Context, usv UnionSassValue) UnionSassValue {
			res, _ := Marshal("no-repeat")
			return res
		}, &ctx,
	}
	err := ctx.Compile(in, &out)
	if err != nil {
		panic(err)
	}

	fmt.Print(out.String())
	// // Output:
	// div {
	//   color: 0;
	//   background: no-repeat; }
}

func BenchmarkContextCompile(b *testing.B) {
	// TBD
}
<commit_msg>create img test directory<commit_after>package context

import (
	"bytes"
	"fmt"
	"image/color"
	"io"
	"io/ioutil"
	"log"
	"os"
	"path/filepath"
	"reflect"
	"regexp"
	"testing"

	"github.com/drewwells/spritewell"
)

func cleanUpSprites(sprites map[string]spritewell.ImageList) {
	if sprites == nil {
		return
	}
	for _, iml := range sprites {
		err := os.Remove(filepath.Join(iml.GenImgDir, iml.OutFile))
		if err != nil {
			log.Fatal(err)
		}
	}
}

func fileString(path string) string {
	bytes, err := ioutil.ReadFile(path)
	if err != nil {
		panic(err)
	}
	return string(bytes)
}

func fileReader(path string) io.Reader {
	reader, err := os.Open(path)
	if err != nil {
		panic(err)
	}
	return reader
}

var rerandom *regexp.Regexp

func init() {
	// Setup build directory
	os.MkdirAll("test/build/img", 0755)
	rerandom = regexp.MustCompile(`-\w{6}(?:\.(png|jpg))`)

}

func TestContextFile(t *testing.T) {

	in := bytes.NewBufferString(`div {
  span {
    color: black;
  }
  width: 100px;
  height: 100px;
}

p {
	background: red;
}`)

	var out bytes.Buffer
	ctx := Context{}
	err := ctx.Compile(in, &out)
	if err != nil {
		panic(err)
	}

	e := `div {
  width: 100px;
  height: 100px; }
  div span {
    color: black; }

p {
  background: red; }
`

	if e != out.String() {
		t.Errorf("wanted:\n%s\n"+
			"got:\n%s\n", e, out.String())
	}
}

func TestContextNilRun(t *testing.T) {
	var in, out bytes.Buffer
	ctx := Context{}
	err := ctx.Compile(&in, &out)
	if err == nil {
		t.Error("No error returned")
	}
	if e := "No input provided"; e != err.Error() {
		t.Errorf("wanted:\n%s\ngot:\n%s", e, err)
	}
}

func TestContextRun(t *testing.T) {

	in := bytes.NewBufferString(`$red-var: red;
$hex: #00FF00;
div {
  background: $hex;
  $hex: #00DD00;
  font-size: 10pt;
}
`)

	var out bytes.Buffer
	ctx := Context{}
	err := ctx.Compile(in, &out)
	if err != nil {
		panic(err)
	}

	e := `div {
  background: #00FF00;
  font-size: 10pt; }
`

	if e != out.String() {
		t.Errorf("wanted:\n%s\n"+
			"got:\n%s\n", e, out.String())
	}

}

func TestContextCustomSimpleTypes(t *testing.T) {
	in := bytes.NewBufferString(`div {
  background: foo(null, 3, asdf, false, #005500);
}`)

	//var out bytes.Buffer

	ctx := Context{}
	ctx.Cookies = make([]Cookie, 1)
	// Communication channel for the C Sass callback function
	ch := make(chan []interface{}, 1)
	ctx.Cookies[0] = Cookie{
		"foo($null, $num, $str, $bool, $color)", func(c *Context, usv UnionSassValue) UnionSassValue {
			// Send the interface fn arguments to the ch channel

			var n interface{}
			var num float64
			var s string
			var b bool
			var col = color.RGBA{}
			var intf = []interface{}{n, num, s, b, col}
			Unmarshal(usv, &intf)
			ch <- intf
			res, _ := Marshal(false)
			return res
		}, &ctx,
	}
	var out bytes.Buffer
	err := ctx.Compile(in, &out)
	if err != nil {
		t.Error(err)
	}

	e := []interface{}{
		"<nil>",
		3.0,
		"asdf",
		false,
		color.RGBA{R: 0x0, G: 0x55, B: 0x0, A: 0x1},
	}
	args := <-ch
	if !reflect.DeepEqual(e, args) {
		t.Errorf("wanted:\n% #v\ngot:\n% #v", e, args)
	}
}

func TestContextCustomComplexTypes(t *testing.T) {
	in := bytes.NewBufferString(`div {
	     background: foo((a,b,1,#003300));
	   }`)

	var out bytes.Buffer
	ctx := Context{}
	if ctx.Cookies == nil {
		ctx.Cookies = make([]Cookie, 1)
	}
	ch := make(chan interface{}, 1)
	ctx.Cookies[0] = Cookie{
		"foo($list)", func(c *Context, usv UnionSassValue) UnionSassValue {
			var sv interface{}
			Unmarshal(usv, &sv)
			ch <- sv
			res, _ := Marshal(false)
			return res
		}, &ctx,
	}
	err := ctx.Compile(in, &out)
	if err != nil {
		t.Error(err)
	}

	e := []interface{}{
		"a",
		"b",
		float64(1),
		color.RGBA{R: 0x0, G: 0x33, B: 0x0, A: 0x1},
	}
	args := <-ch
	if !reflect.DeepEqual(e, args) {
		t.Errorf("wanted:\n%#v\ngot:\n% #v", e, args)
	}
}

func TestContextCustomArity(t *testing.T) {
	in := bytes.NewBufferString(`div {
  color: red(blue);
  background: foo(1, 2);
}`)

	var out bytes.Buffer
	ctx := Context{}
	if ctx.Cookies == nil {
		ctx.Cookies = make([]Cookie, 1)
	}

	ctx.Cookies[0] = Cookie{
		"foo()", SampleCB, &ctx,
	}
	err := ctx.Compile(in, &out)
	if err == nil {
		t.Skip("No error thrown for incorrect arity")
	}

	if e := "function foo only takes 0 arguments; given 2"; e != ctx.Errors.Message {
		t.Skipf("wanted:\n%s\ngot:\n%s\n", e, ctx.Errors.Message)
	}
}

func ExampleContext_Compile() {
	in := bytes.NewBufferString(`div {
			  color: red(blue);
			  background: foo();
			}`)

	var out bytes.Buffer
	ctx := Context{
	//Customs: []string{"foo()"},
	}
	if ctx.Cookies == nil {
		ctx.Cookies = make([]Cookie, 1)
	}
	ctx.Cookies[0] = Cookie{
		"foo()", func(c *Context, usv UnionSassValue) UnionSassValue {
			res, _ := Marshal("no-repeat")
			return res
		}, &ctx,
	}
	err := ctx.Compile(in, &out)
	if err != nil {
		panic(err)
	}

	fmt.Print(out.String())
	// // Output:
	// div {
	//   color: 0;
	//   background: no-repeat; }
}

func BenchmarkContextCompile(b *testing.B) {
	// TBD
}
<|endoftext|>


<commit_before>package watch

import (
	"fmt"
	"log"
	"reflect"
	"time"

	"github.com/hashicorp/consul-template/dependency"
	"github.com/hashicorp/consul/api"
)

const (
	// The amount of time to do a blocking query for
	defaultWaitTime = 60 * time.Second

	// The amount of time to wait when Consul returns an error
	defaultRetry = 1 * time.Second
)

// View is a representation of a Dependency and the most recent data it has
// received from Consul.
type View struct {
	// Dependency is the dependency that is associated with this View
	Dependency dependency.Dependency

	// Data is the most-recently-received data from Consul for this View
	Data         interface{}
	receivedData bool
	lastIndex    uint64

	// client is the Consul API client
	client *api.Client

	// stopCh is used to stop polling on this View
	stopCh chan struct{}
}

// NewView creates a new view object from the given Consul API client and
// Dependency. If an error occurs, it will be returned.
func NewView(client *api.Client, dep dependency.Dependency) (*View, error) {
	if client == nil {
		return nil, fmt.Errorf("view: missing Consul API client")
	}

	if dep == nil {
		return nil, fmt.Errorf("view: missing Dependency")
	}

	return &View{
		Dependency: dep,
		client:     client,
		stopCh:     make(chan struct{}),
	}, nil
}

// poll queries the Consul instance for data using the fetch function, but also
// accounts for interrupts on the interrupt channel. This allows the poll
// function to be fired in a goroutine, but then halted even if the fetch
// function is in the middle of a blocking query.
func (v *View) poll(once bool, viewCh chan<- *View, errCh chan<- error, retryFunc RetryFunc) {
	currentRetry := defaultRetry

	for {
		doneCh, fetchErrCh := make(chan struct{}, 1), make(chan error, 1)
		go v.fetch(doneCh, fetchErrCh)

		select {
		case <-doneCh:
			// Reset the retry to avoid exponentially incrementing retries when we
			// have some successful requests
			currentRetry = defaultRetry

			log.Printf("[INFO] (%s) received data from consul", v.display())
			viewCh <- v

			// If we are operating in once mode, do not loop - we received data at
			// least once which is the API promise here.
			if once {
				return
			}
		case err := <-fetchErrCh:
			log.Printf("[ERR] (%s) %s", v.display(), err)

			// Intentionally do not send the error back up to the watcher and just
			// retry. Eventually, once Consul API implements errwrap and multierror,
			// we can check the "type" of error and conditionally alert back, but for
			// now we will just swallow the error and continue.
			// if err.Contains(Something) {
			//   errCh <- err
			// }

			// Sleep and retry
			currentRetry = retryFunc(currentRetry)
			time.Sleep(currentRetry)
			continue
		case <-v.stopCh:
			log.Printf("[DEBUG] (%s) stopping poll (received on view stopCh)", v.display())
			return
		}
	}
}

// fetch queries the Consul instance for the attached dependency. This API
// promises that either data will be written to doneCh or an error will be
// written to errCh. It is designed to be run in a goroutine that selects the
// result of doneCh and errCh. It is assumed that only one instance of fetch
// is running per View and therefore no locking or mutexes are used.
func (v *View) fetch(doneCh chan<- struct{}, errCh chan<- error) {
	log.Printf("[DEBUG] (%s) starting fetch", v.display())

	for {
		options := &api.QueryOptions{
			WaitTime:  defaultWaitTime,
			WaitIndex: v.lastIndex,
		}
		data, qm, err := v.Dependency.Fetch(v.client, options)
		if err != nil {
			errCh <- err
			return
		}

		if qm == nil {
			errCh <- fmt.Errorf("consul returned nil qm; this should never happen" +
				"and is probably a bug in consul-template or consulapi")
			return
		}

		if qm.LastIndex == v.lastIndex {
			log.Printf("[DEBUG] (%s) no new data (index was the same)", v.display())
			continue
		}

		v.lastIndex = qm.LastIndex

		if v.receivedData && reflect.DeepEqual(data, v.Data) {
			log.Printf("[DEBUG] (%s) no new data (contents were the same)", v.display())
			continue
		}

		v.Data = data
		v.receivedData = true
		close(doneCh)
		return
	}
}

// display returns a string that represents this view.
func (v *View) display() string {
	return v.Dependency.Display()
}

// stop halts polling of this view.
func (v *View) stop() {
	close(v.stopCh)
}
<commit_msg>Clarify log output from view<commit_after>package watch

import (
	"fmt"
	"log"
	"reflect"
	"time"

	"github.com/hashicorp/consul-template/dependency"
	"github.com/hashicorp/consul/api"
)

const (
	// The amount of time to do a blocking query for
	defaultWaitTime = 60 * time.Second

	// The amount of time to wait when Consul returns an error
	defaultRetry = 1 * time.Second
)

// View is a representation of a Dependency and the most recent data it has
// received from Consul.
type View struct {
	// Dependency is the dependency that is associated with this View
	Dependency dependency.Dependency

	// Data is the most-recently-received data from Consul for this View
	Data         interface{}
	receivedData bool
	lastIndex    uint64

	// client is the Consul API client
	client *api.Client

	// stopCh is used to stop polling on this View
	stopCh chan struct{}
}

// NewView creates a new view object from the given Consul API client and
// Dependency. If an error occurs, it will be returned.
func NewView(client *api.Client, dep dependency.Dependency) (*View, error) {
	if client == nil {
		return nil, fmt.Errorf("view: missing Consul API client")
	}

	if dep == nil {
		return nil, fmt.Errorf("view: missing Dependency")
	}

	return &View{
		Dependency: dep,
		client:     client,
		stopCh:     make(chan struct{}),
	}, nil
}

// poll queries the Consul instance for data using the fetch function, but also
// accounts for interrupts on the interrupt channel. This allows the poll
// function to be fired in a goroutine, but then halted even if the fetch
// function is in the middle of a blocking query.
func (v *View) poll(once bool, viewCh chan<- *View, errCh chan<- error, retryFunc RetryFunc) {
	currentRetry := defaultRetry

	for {
		doneCh, fetchErrCh := make(chan struct{}, 1), make(chan error, 1)
		go v.fetch(doneCh, fetchErrCh)

		select {
		case <-doneCh:
			// Reset the retry to avoid exponentially incrementing retries when we
			// have some successful requests
			currentRetry = defaultRetry

			log.Printf("[INFO] (view) %s received data from consul", v.display())
			viewCh <- v

			// If we are operating in once mode, do not loop - we received data at
			// least once which is the API promise here.
			if once {
				return
			}
		case err := <-fetchErrCh:
			log.Printf("[ERR] (view) %s %s", v.display(), err)

			// Intentionally do not send the error back up to the watcher and just
			// retry. Eventually, once Consul API implements errwrap and multierror,
			// we can check the "type" of error and conditionally alert back, but for
			// now we will just swallow the error and continue.
			// if err.Contains(Something) {
			//   errCh <- err
			// }

			// Sleep and retry
			currentRetry = retryFunc(currentRetry)
			time.Sleep(currentRetry)
			continue
		case <-v.stopCh:
			log.Printf("[DEBUG] (view) %s stopping poll (received on view stopCh)", v.display())
			return
		}
	}
}

// fetch queries the Consul instance for the attached dependency. This API
// promises that either data will be written to doneCh or an error will be
// written to errCh. It is designed to be run in a goroutine that selects the
// result of doneCh and errCh. It is assumed that only one instance of fetch
// is running per View and therefore no locking or mutexes are used.
func (v *View) fetch(doneCh chan<- struct{}, errCh chan<- error) {
	log.Printf("[DEBUG] (view) %s starting fetch", v.display())

	for {
		options := &api.QueryOptions{
			WaitTime:  defaultWaitTime,
			WaitIndex: v.lastIndex,
		}
		data, qm, err := v.Dependency.Fetch(v.client, options)
		if err != nil {
			errCh <- err
			return
		}

		if qm == nil {
			errCh <- fmt.Errorf("consul returned nil qm; this should never happen" +
				"and is probably a bug in consul-template or consulapi")
			return
		}

		if qm.LastIndex == v.lastIndex {
			log.Printf("[DEBUG] (view) %s no new data (index was the same)", v.display())
			continue
		}

		v.lastIndex = qm.LastIndex

		if v.receivedData && reflect.DeepEqual(data, v.Data) {
			log.Printf("[DEBUG] (view) %s no new data (contents were the same)", v.display())
			continue
		}

		v.Data = data
		v.receivedData = true
		close(doneCh)
		return
	}
}

// display returns a string that represents this view.
func (v *View) display() string {
	return v.Dependency.Display()
}

// stop halts polling of this view.
func (v *View) stop() {
	close(v.stopCh)
}
<|endoftext|>


<commit_before>package terraform

import (
	"errors"
	"strings"
	"testing"

	"github.com/hashicorp/hcl2/hcl"
	"github.com/hashicorp/terraform/config/configschema"
	"github.com/hashicorp/terraform/tfdiags"

	"github.com/hashicorp/terraform/configs"
	"github.com/zclconf/go-cty/cty"

	"github.com/hashicorp/terraform/addrs"
)

func TestEvalValidateResource_managedResource(t *testing.T) {
	mp := testProvider("aws")
	mp.ValidateResourceFn = func(rt string, c *ResourceConfig) (ws []string, es []error) {
		expected := "aws_instance"
		if rt != expected {
			t.Fatalf("expected: %s, got: %s", expected, rt)
		}
		expected = "bar"
		val, _ := c.Get("foo")
		if val != expected {
			t.Fatalf("expected: %s, got: %s", expected, val)
		}
		return
	}

	p := ResourceProvider(mp)
	rc := &configs.Resource{
		Mode: addrs.ManagedResourceMode,
		Type: "aws_instance",
		Name: "foo",
		Config: configs.SynthBody("", map[string]cty.Value{
			"foo": cty.StringVal("bar"),
		}),
	}
	node := &EvalValidateResource{
		Addr: addrs.ResourceInstance{
			Resource: addrs.Resource{
				Mode: addrs.ManagedResourceMode,
				Type: "aws_instance",
				Name: "foo",
			},
		},
		Provider: &p,
		Config:   rc,
	}

	_, err := node.Eval(&MockEvalContext{})
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if !mp.ValidateResourceCalled {
		t.Fatal("Expected ValidateResource to be called, but it was not!")
	}
}

func TestEvalValidateResource_dataSource(t *testing.T) {
	mp := testProvider("aws")
	mp.ValidateDataSourceFn = func(rt string, c *ResourceConfig) (ws []string, es []error) {
		expected := "aws_ami"
		if rt != expected {
			t.Fatalf("expected: %s, got: %s", expected, rt)
		}
		expected = "bar"
		val, _ := c.Get("foo")
		if val != expected {
			t.Fatalf("expected: %s, got: %s", expected, val)
		}
		return
	}

	p := ResourceProvider(mp)
	rc := &configs.Resource{
		Mode: addrs.DataResourceMode,
		Type: "aws_ami",
		Name: "foo",
		Config: configs.SynthBody("", map[string]cty.Value{
			"foo": cty.StringVal("bar"),
		}),
	}

	node := &EvalValidateResource{
		Addr: addrs.ResourceInstance{
			Resource: addrs.Resource{
				Mode: addrs.DataResourceMode,
				Type: "aws_ami",
				Name: "foo",
			},
		},
		Provider: &p,
		Config:   rc,
	}

	_, err := node.Eval(&MockEvalContext{})
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if !mp.ValidateDataSourceCalled {
		t.Fatal("Expected ValidateDataSource to be called, but it was not!")
	}
}

func TestEvalValidateResource_validReturnsNilError(t *testing.T) {
	mp := testProvider("aws")
	mp.ValidateResourceFn = func(rt string, c *ResourceConfig) (ws []string, es []error) {
		return
	}

	p := ResourceProvider(mp)
	rc := &configs.Resource{
		Mode:   addrs.ManagedResourceMode,
		Type:   "aws_instance",
		Name:   "foo",
		Config: configs.SynthBody("", map[string]cty.Value{}),
	}
	node := &EvalValidateResource{
		Addr: addrs.ResourceInstance{
			Resource: addrs.Resource{
				Mode: addrs.ManagedResourceMode,
				Type: "aws_instance",
				Name: "foo",
			},
		},
		Provider: &p,
		Config:   rc,
	}

	_, err := node.Eval(&MockEvalContext{})
	if err != nil {
		t.Fatalf("Expected nil error, got: %s", err)
	}
}

func TestEvalValidateResource_warningsAndErrorsPassedThrough(t *testing.T) {
	mp := testProvider("aws")
	mp.ValidateResourceFn = func(rt string, c *ResourceConfig) (ws []string, es []error) {
		ws = append(ws, "warn")
		es = append(es, errors.New("err"))
		return
	}

	p := ResourceProvider(mp)
	rc := &configs.Resource{
		Mode:   addrs.ManagedResourceMode,
		Type:   "aws_instance",
		Name:   "foo",
		Config: configs.SynthBody("", map[string]cty.Value{}),
	}
	node := &EvalValidateResource{
		Addr: addrs.ResourceInstance{
			Resource: addrs.Resource{
				Mode: addrs.ManagedResourceMode,
				Type: "aws_instance",
				Name: "foo",
			},
		},
		Provider: &p,
		Config:   rc,
	}

	_, err := node.Eval(&MockEvalContext{})
	if err == nil {
		t.Fatal("Expected an error, got none!")
	}

	var diags tfdiags.Diagnostics
	diags = diags.Append(err)
	bySeverity := map[tfdiags.Severity]tfdiags.Diagnostics{}
	for _, diag := range diags {
		bySeverity[diag.Severity()] = append(bySeverity[diag.Severity()], diag)
	}
	if len(bySeverity[tfdiags.Warning]) != 1 || bySeverity[tfdiags.Warning][0].Description().Summary != "warn" {
		t.Fatalf("Expected 1 warning 'warn', got: %#v", bySeverity[tfdiags.Warning])
	}
	if len(bySeverity[tfdiags.Error]) != 1 || bySeverity[tfdiags.Error][0].Description().Summary != "err" {
		t.Fatalf("Expected 1 error 'err', got: %#v", bySeverity[tfdiags.Error])
	}
}

func TestEvalValidateResource_ignoreWarnings(t *testing.T) {
	mp := testProvider("aws")
	mp.ValidateResourceFn = func(rt string, c *ResourceConfig) (ws []string, es []error) {
		ws = append(ws, "warn")
		return
	}

	p := ResourceProvider(mp)
	rc := &configs.Resource{
		Mode:   addrs.ManagedResourceMode,
		Type:   "aws_instance",
		Name:   "foo",
		Config: configs.SynthBody("", map[string]cty.Value{}),
	}
	node := &EvalValidateResource{
		Addr: addrs.ResourceInstance{
			Resource: addrs.Resource{
				Mode: addrs.ManagedResourceMode,
				Type: "aws_instance",
				Name: "foo",
			},
		},
		Provider: &p,
		Config:   rc,

		IgnoreWarnings: true,
	}

	_, err := node.Eval(&MockEvalContext{})
	if err != nil {
		t.Fatalf("Expected no error, got: %s", err)
	}
}

func TestEvalValidateProvisioner_valid(t *testing.T) {
	mp := &MockResourceProvisioner{}
	var p ResourceProvisioner = mp
	ctx := &MockEvalContext{}

	schema := &configschema.Block{}

	node := &EvalValidateProvisioner{
		ResourceAddr: addrs.ResourceInstance{
			Resource: addrs.Resource{
				Mode: addrs.ManagedResourceMode,
				Type: "foo",
				Name: "bar",
			},
		},
		Provisioner: &p,
		Schema:      &schema,
		Config: &configs.Provisioner{
			Type:   "baz",
			Config: hcl.EmptyBody(),
		},
		ConnConfig: &configs.Connection{
			Type:   "ssh",
			Config: hcl.EmptyBody(),
		},
	}

	result, err := node.Eval(ctx)
	if err != nil {
		t.Fatalf("node.Eval failed: %s", err)
	}
	if result != nil {
		t.Errorf("node.Eval returned non-nil result")
	}

	if !mp.ValidateCalled {
		t.Fatalf("p.Validate not called")
	}
}

func TestEvalValidateProvisioner_warning(t *testing.T) {
	mp := &MockResourceProvisioner{}
	var p ResourceProvisioner = mp
	ctx := &MockEvalContext{}

	schema := &configschema.Block{}

	node := &EvalValidateProvisioner{
		ResourceAddr: addrs.ResourceInstance{
			Resource: addrs.Resource{
				Mode: addrs.ManagedResourceMode,
				Type: "foo",
				Name: "bar",
			},
		},
		Provisioner: &p,
		Schema:      &schema,
		Config: &configs.Provisioner{
			Type:   "baz",
			Config: hcl.EmptyBody(),
		},
		ConnConfig: &configs.Connection{
			Type:   "ssh",
			Config: hcl.EmptyBody(),
		},
	}

	mp.ValidateReturnWarns = []string{"foo is deprecated"}

	_, err := node.Eval(ctx)
	if err == nil {
		t.Fatalf("node.Eval succeeded; want error")
	}

	var diags tfdiags.Diagnostics
	diags = diags.Append(err)
	if len(diags) != 1 {
		t.Fatalf("wrong number of diagsnostics in %#v; want one warning", diags)
	}

	if got, want := diags[0].Description().Summary, mp.ValidateReturnWarns[0]; got != want {
		t.Fatalf("wrong warning %q; want %q", got, want)
	}
}

func TestEvalValidateProvisioner_connectionInvalid(t *testing.T) {
	var p ResourceProvisioner = &MockResourceProvisioner{}
	ctx := &MockEvalContext{}

	schema := &configschema.Block{}

	node := &EvalValidateProvisioner{
		ResourceAddr: addrs.ResourceInstance{
			Resource: addrs.Resource{
				Mode: addrs.ManagedResourceMode,
				Type: "foo",
				Name: "bar",
			},
		},
		Provisioner: &p,
		Schema:      &schema,
		Config: &configs.Provisioner{
			Type:   "baz",
			Config: hcl.EmptyBody(),
		},
		ConnConfig: &configs.Connection{
			Type: "ssh",
			Config: configs.SynthBody("", map[string]cty.Value{
				"bananananananana": cty.StringVal("foo"),
				"bazaz":            cty.StringVal("bar"),
			}),
		},
	}

	_, err := node.Eval(ctx)
	if err == nil {
		t.Fatalf("node.Eval succeeded; want error")
	}

	var diags tfdiags.Diagnostics
	diags = diags.Append(err)
	if len(diags) != 2 {
		t.Fatalf("wrong number of diagsnostics in %#v; want two errors", diags)
	}

	errStr := diags.Err().Error()
	if !(strings.Contains(errStr, "bananananananana") && strings.Contains(errStr, "bazaz")) {
		t.Fatalf("wrong errors %q; want something about each of our invalid connInfo keys", errStr)
	}
}
<commit_msg>core: Fix tests for types in eval_validate.go<commit_after>package terraform

import (
	"errors"
	"strings"
	"testing"

	"github.com/hashicorp/hcl2/hcl"
	"github.com/zclconf/go-cty/cty"

	"github.com/hashicorp/terraform/addrs"
	"github.com/hashicorp/terraform/config/configschema"
	"github.com/hashicorp/terraform/configs"
	"github.com/hashicorp/terraform/tfdiags"
)

func TestEvalValidateResource_managedResource(t *testing.T) {
	mp := simpleMockProvider()
	mp.ValidateResourceFn = func(rt string, c *ResourceConfig) (ws []string, es []error) {
		expected := "test_object"
		if rt != expected {
			t.Fatalf("wrong resource type\ngot:  %#v\nwant: %#v", rt, expected)
		}
		expected = "bar"
		val, _ := c.Get("test_string")
		if val != expected {
			t.Fatalf("wrong value for test_string\ngot:  %#v\nwant: %#v", val, expected)
		}
		return
	}

	p := ResourceProvider(mp)
	rc := &configs.Resource{
		Mode: addrs.ManagedResourceMode,
		Type: "test_object",
		Name: "foo",
		Config: configs.SynthBody("", map[string]cty.Value{
			"test_string": cty.StringVal("bar"),
		}),
	}
	node := &EvalValidateResource{
		Addr: addrs.ResourceInstance{
			Resource: addrs.Resource{
				Mode: addrs.ManagedResourceMode,
				Type: "aws_instance",
				Name: "foo",
			},
		},
		Provider:       &p,
		Config:         rc,
		ProviderSchema: &mp.GetSchemaReturn,
	}

	ctx := &MockEvalContext{}
	ctx.installSimpleEval()

	_, err := node.Eval(ctx)
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if !mp.ValidateResourceCalled {
		t.Fatal("Expected ValidateResource to be called, but it was not!")
	}
}

func TestEvalValidateResource_dataSource(t *testing.T) {
	mp := simpleMockProvider()
	mp.ValidateDataSourceFn = func(rt string, c *ResourceConfig) (ws []string, es []error) {
		expected := "test_object"
		if rt != expected {
			t.Fatalf("expected: %s, got: %s", expected, rt)
		}
		expected = "bar"
		val, _ := c.Get("test_string")
		if val != expected {
			t.Fatalf("expected: %s, got: %s", expected, val)
		}
		return
	}

	p := ResourceProvider(mp)
	rc := &configs.Resource{
		Mode: addrs.DataResourceMode,
		Type: "test_object",
		Name: "foo",
		Config: configs.SynthBody("", map[string]cty.Value{
			"test_string": cty.StringVal("bar"),
		}),
	}

	node := &EvalValidateResource{
		Addr: addrs.ResourceInstance{
			Resource: addrs.Resource{
				Mode: addrs.DataResourceMode,
				Type: "aws_ami",
				Name: "foo",
			},
		},
		Provider:       &p,
		Config:         rc,
		ProviderSchema: &mp.GetSchemaReturn,
	}

	ctx := &MockEvalContext{}
	ctx.installSimpleEval()

	_, err := node.Eval(ctx)
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if !mp.ValidateDataSourceCalled {
		t.Fatal("Expected ValidateDataSource to be called, but it was not!")
	}
}

func TestEvalValidateResource_validReturnsNilError(t *testing.T) {
	mp := simpleMockProvider()
	mp.ValidateResourceFn = func(rt string, c *ResourceConfig) (ws []string, es []error) {
		return
	}

	p := ResourceProvider(mp)
	rc := &configs.Resource{
		Mode:   addrs.ManagedResourceMode,
		Type:   "test_object",
		Name:   "foo",
		Config: configs.SynthBody("", map[string]cty.Value{}),
	}
	node := &EvalValidateResource{
		Addr: addrs.ResourceInstance{
			Resource: addrs.Resource{
				Mode: addrs.ManagedResourceMode,
				Type: "test_object",
				Name: "foo",
			},
		},
		Provider:       &p,
		Config:         rc,
		ProviderSchema: &mp.GetSchemaReturn,
	}

	ctx := &MockEvalContext{}
	ctx.installSimpleEval()

	_, err := node.Eval(ctx)
	if err != nil {
		t.Fatalf("Expected nil error, got: %s", err)
	}
}

func TestEvalValidateResource_warningsAndErrorsPassedThrough(t *testing.T) {
	mp := simpleMockProvider()
	mp.ValidateResourceFn = func(rt string, c *ResourceConfig) (ws []string, es []error) {
		ws = append(ws, "warn")
		es = append(es, errors.New("err"))
		return
	}

	p := ResourceProvider(mp)
	rc := &configs.Resource{
		Mode:   addrs.ManagedResourceMode,
		Type:   "test_object",
		Name:   "foo",
		Config: configs.SynthBody("", map[string]cty.Value{}),
	}
	node := &EvalValidateResource{
		Addr: addrs.ResourceInstance{
			Resource: addrs.Resource{
				Mode: addrs.ManagedResourceMode,
				Type: "test_object",
				Name: "foo",
			},
		},
		Provider:       &p,
		Config:         rc,
		ProviderSchema: &mp.GetSchemaReturn,
	}

	ctx := &MockEvalContext{}
	ctx.installSimpleEval()

	_, err := node.Eval(ctx)
	if err == nil {
		t.Fatal("unexpected success; want error")
	}

	var diags tfdiags.Diagnostics
	diags = diags.Append(err)
	bySeverity := map[tfdiags.Severity]tfdiags.Diagnostics{}
	for _, diag := range diags {
		bySeverity[diag.Severity()] = append(bySeverity[diag.Severity()], diag)
	}
	if len(bySeverity[tfdiags.Warning]) != 1 || bySeverity[tfdiags.Warning][0].Description().Summary != "warn" {
		t.Errorf("Expected 1 warning 'warn', got: %s", bySeverity[tfdiags.Warning].ErrWithWarnings())
	}
	if len(bySeverity[tfdiags.Error]) != 1 || bySeverity[tfdiags.Error][0].Description().Summary != "err" {
		t.Errorf("Expected 1 error 'err', got: %s", bySeverity[tfdiags.Error].Err())
	}
}

func TestEvalValidateResource_ignoreWarnings(t *testing.T) {
	mp := simpleMockProvider()
	mp.ValidateResourceFn = func(rt string, c *ResourceConfig) (ws []string, es []error) {
		ws = append(ws, "warn")
		return
	}

	p := ResourceProvider(mp)
	rc := &configs.Resource{
		Mode:   addrs.ManagedResourceMode,
		Type:   "test_object",
		Name:   "foo",
		Config: configs.SynthBody("", map[string]cty.Value{}),
	}
	node := &EvalValidateResource{
		Addr: addrs.ResourceInstance{
			Resource: addrs.Resource{
				Mode: addrs.ManagedResourceMode,
				Type: "test-object",
				Name: "foo",
			},
		},
		Provider:       &p,
		Config:         rc,
		ProviderSchema: &mp.GetSchemaReturn,

		IgnoreWarnings: true,
	}

	ctx := &MockEvalContext{}
	ctx.installSimpleEval()

	_, err := node.Eval(ctx)
	if err != nil {
		t.Fatalf("Expected no error, got: %s", err)
	}
}

func TestEvalValidateProvisioner_valid(t *testing.T) {
	mp := &MockResourceProvisioner{}
	var p ResourceProvisioner = mp
	ctx := &MockEvalContext{}
	ctx.installSimpleEval()

	schema := &configschema.Block{}

	node := &EvalValidateProvisioner{
		ResourceAddr: addrs.ResourceInstance{
			Resource: addrs.Resource{
				Mode: addrs.ManagedResourceMode,
				Type: "foo",
				Name: "bar",
			},
		},
		Provisioner: &p,
		Schema:      &schema,
		Config: &configs.Provisioner{
			Type:   "baz",
			Config: hcl.EmptyBody(),
		},
		ConnConfig: &configs.Connection{
			Type:   "ssh",
			Config: hcl.EmptyBody(),
		},
	}

	result, err := node.Eval(ctx)
	if err != nil {
		t.Fatalf("node.Eval failed: %s", err)
	}
	if result != nil {
		t.Errorf("node.Eval returned non-nil result")
	}

	if !mp.ValidateCalled {
		t.Fatalf("p.Validate not called")
	}
}

func TestEvalValidateProvisioner_warning(t *testing.T) {
	mp := &MockResourceProvisioner{}
	var p ResourceProvisioner = mp
	ctx := &MockEvalContext{}
	ctx.installSimpleEval()

	schema := &configschema.Block{}

	node := &EvalValidateProvisioner{
		ResourceAddr: addrs.ResourceInstance{
			Resource: addrs.Resource{
				Mode: addrs.ManagedResourceMode,
				Type: "foo",
				Name: "bar",
			},
		},
		Provisioner: &p,
		Schema:      &schema,
		Config: &configs.Provisioner{
			Type:   "baz",
			Config: hcl.EmptyBody(),
		},
		ConnConfig: &configs.Connection{
			Type:   "ssh",
			Config: hcl.EmptyBody(),
		},
	}

	mp.ValidateReturnWarns = []string{"foo is deprecated"}

	_, err := node.Eval(ctx)
	if err == nil {
		t.Fatalf("node.Eval succeeded; want error")
	}

	var diags tfdiags.Diagnostics
	diags = diags.Append(err)
	if len(diags) != 1 {
		t.Fatalf("wrong number of diagsnostics in %#v; want one warning", diags)
	}

	if got, want := diags[0].Description().Summary, mp.ValidateReturnWarns[0]; got != want {
		t.Fatalf("wrong warning %q; want %q", got, want)
	}
}

func TestEvalValidateProvisioner_connectionInvalid(t *testing.T) {
	var p ResourceProvisioner = &MockResourceProvisioner{}
	ctx := &MockEvalContext{}
	ctx.installSimpleEval()

	schema := &configschema.Block{}

	node := &EvalValidateProvisioner{
		ResourceAddr: addrs.ResourceInstance{
			Resource: addrs.Resource{
				Mode: addrs.ManagedResourceMode,
				Type: "foo",
				Name: "bar",
			},
		},
		Provisioner: &p,
		Schema:      &schema,
		Config: &configs.Provisioner{
			Type:   "baz",
			Config: hcl.EmptyBody(),
		},
		ConnConfig: &configs.Connection{
			Type: "ssh",
			Config: configs.SynthBody("", map[string]cty.Value{
				"bananananananana": cty.StringVal("foo"),
				"bazaz":            cty.StringVal("bar"),
			}),
		},
	}

	_, err := node.Eval(ctx)
	if err == nil {
		t.Fatalf("node.Eval succeeded; want error")
	}

	var diags tfdiags.Diagnostics
	diags = diags.Append(err)
	if len(diags) != 2 {
		t.Fatalf("wrong number of diagnostics; want two errors\n\n%s", diags.Err())
	}

	errStr := diags.Err().Error()
	if !(strings.Contains(errStr, "bananananananana") && strings.Contains(errStr, "bazaz")) {
		t.Fatalf("wrong errors %q; want something about each of our invalid connInfo keys", errStr)
	}
}
<|endoftext|>


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

// Extract example functions from file ASTs.

package doc

import (
	"go/ast"
	"go/token"
	"path"
	"regexp"
	"sort"
	"strconv"
	"strings"
	"unicode"
	"unicode/utf8"
)

type Example struct {
	Name     string // name of the item being exemplified
	Doc      string // example function doc string
	Code     ast.Node
	Play     *ast.File // a whole program version of the example
	Comments []*ast.CommentGroup
	Output   string // expected output
}

func Examples(files ...*ast.File) []*Example {
	var list []*Example
	for _, file := range files {
		hasTests := false // file contains tests or benchmarks
		numDecl := 0      // number of non-import declarations in the file
		var flist []*Example
		for _, decl := range file.Decls {
			if g, ok := decl.(*ast.GenDecl); ok && g.Tok != token.IMPORT {
				numDecl++
				continue
			}
			f, ok := decl.(*ast.FuncDecl)
			if !ok {
				continue
			}
			numDecl++
			name := f.Name.Name
			if isTest(name, "Test") || isTest(name, "Benchmark") {
				hasTests = true
				continue
			}
			if !isTest(name, "Example") {
				continue
			}
			var doc string
			if f.Doc != nil {
				doc = f.Doc.Text()
			}
			flist = append(flist, &Example{
				Name:     name[len("Example"):],
				Doc:      doc,
				Code:     f.Body,
				Play:     playExample(file, f.Body),
				Comments: file.Comments,
				Output:   exampleOutput(f.Body, file.Comments),
			})
		}
		if !hasTests && numDecl > 1 && len(flist) == 1 {
			// If this file only has one example function, some
			// other top-level declarations, and no tests or
			// benchmarks, use the whole file as the example.
			flist[0].Code = file
			flist[0].Play = playExampleFile(file)
		}
		list = append(list, flist...)
	}
	sort.Sort(exampleByName(list))
	return list
}

var outputPrefix = regexp.MustCompile(`(?i)^[[:space:]]*output:`)

func exampleOutput(b *ast.BlockStmt, comments []*ast.CommentGroup) string {
	if _, last := lastComment(b, comments); last != nil {
		// test that it begins with the correct prefix
		text := last.Text()
		if loc := outputPrefix.FindStringIndex(text); loc != nil {
			return strings.TrimSpace(text[loc[1]:])
		}
	}
	return "" // no suitable comment found
}

// isTest tells whether name looks like a test, example, or benchmark.
// It is a Test (say) if there is a character after Test that is not a
// lower-case letter. (We don't want Testiness.)
func isTest(name, prefix string) bool {
	if !strings.HasPrefix(name, prefix) {
		return false
	}
	if len(name) == len(prefix) { // "Test" is ok
		return true
	}
	rune, _ := utf8.DecodeRuneInString(name[len(prefix):])
	return !unicode.IsLower(rune)
}

type exampleByName []*Example

func (s exampleByName) Len() int           { return len(s) }
func (s exampleByName) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }
func (s exampleByName) Less(i, j int) bool { return s[i].Name < s[j].Name }

// playExample synthesizes a new *ast.File based on the provided
// file with the provided function body as the body of main.
func playExample(file *ast.File, body *ast.BlockStmt) *ast.File {
	if !strings.HasSuffix(file.Name.Name, "_test") {
		// We don't support examples that are part of the
		// greater package (yet).
		return nil
	}

	// Find top-level declarations in the file.
	topDecls := make(map[*ast.Object]bool)
	for _, decl := range file.Decls {
		switch d := decl.(type) {
		case *ast.FuncDecl:
			topDecls[d.Name.Obj] = true
		case *ast.GenDecl:
			for _, spec := range d.Specs {
				switch s := spec.(type) {
				case *ast.TypeSpec:
					topDecls[s.Name.Obj] = true
				case *ast.ValueSpec:
					for _, id := range s.Names {
						topDecls[id.Obj] = true
					}
				}
			}
		}
	}

	// Find unresolved identifiers and uses of top-level declarations.
	unresolved := make(map[string]bool)
	usesTopDecl := false
	ast.Inspect(body, func(n ast.Node) bool {
		// For an expression like fmt.Println, only add "fmt" to the
		// set of unresolved names.
		if e, ok := n.(*ast.SelectorExpr); ok {
			if id, ok := e.X.(*ast.Ident); ok && id.Obj == nil {
				unresolved[id.Name] = true
			}
			return false
		}
		if id, ok := n.(*ast.Ident); ok {
			if id.Obj == nil {
				unresolved[id.Name] = true
			} else if topDecls[id.Obj] {
				usesTopDecl = true
			}
		}
		return true
	})
	if usesTopDecl {
		// We don't support examples that are not self-contained (yet).
		return nil
	}

	// Remove predeclared identifiers from unresolved list.
	for n := range unresolved {
		if predeclaredTypes[n] || predeclaredConstants[n] || predeclaredFuncs[n] {
			delete(unresolved, n)
		}
	}

	// Use unresolved identifiers to determine the imports used by this
	// example. The heuristic assumes package names match base import
	// paths for imports w/o renames (should be good enough most of the time).
	namedImports := make(map[string]string) // [name]path
	var blankImports []ast.Spec             // _ imports
	for _, s := range file.Imports {
		p, err := strconv.Unquote(s.Path.Value)
		if err != nil {
			continue
		}
		n := path.Base(p)
		if s.Name != nil {
			n = s.Name.Name
			switch n {
			case "_":
				blankImports = append(blankImports, s)
				continue
			case ".":
				// We can't resolve dot imports (yet).
				return nil
			}
		}
		if unresolved[n] {
			namedImports[n] = p
			delete(unresolved, n)
		}
	}

	// If there are other unresolved identifiers, give up because this
	// synthesized file is not going to build.
	if len(unresolved) > 0 {
		return nil
	}

	// Include documentation belonging to blank imports.
	var comments []*ast.CommentGroup
	for _, s := range blankImports {
		if c := s.(*ast.ImportSpec).Doc; c != nil {
			comments = append(comments, c)
		}
	}

	// Include comments that are inside the function body.
	for _, c := range file.Comments {
		if body.Pos() <= c.Pos() && c.End() <= body.End() {
			comments = append(comments, c)
		}
	}

	// Strip "Output:" commment and adjust body end position.
	body, comments = stripOutputComment(body, comments)

	// Synthesize import declaration.
	importDecl := &ast.GenDecl{
		Tok:    token.IMPORT,
		Lparen: 1, // Need non-zero Lparen and Rparen so that printer
		Rparen: 1, // treats this as a factored import.
	}
	for n, p := range namedImports {
		s := &ast.ImportSpec{Path: &ast.BasicLit{Value: strconv.Quote(p)}}
		if path.Base(p) != n {
			s.Name = ast.NewIdent(n)
		}
		importDecl.Specs = append(importDecl.Specs, s)
	}
	importDecl.Specs = append(importDecl.Specs, blankImports...)

	// Synthesize main function.
	funcDecl := &ast.FuncDecl{
		Name: ast.NewIdent("main"),
		Type: &ast.FuncType{},
		Body: body,
	}

	// Synthesize file.
	return &ast.File{
		Name:     ast.NewIdent("main"),
		Decls:    []ast.Decl{importDecl, funcDecl},
		Comments: comments,
	}
}

// playExampleFile takes a whole file example and synthesizes a new *ast.File
// such that the example is function main in package main.
func playExampleFile(file *ast.File) *ast.File {
	// Strip copyright comment if present.
	comments := file.Comments
	if len(comments) > 0 && strings.HasPrefix(comments[0].Text(), "Copyright") {
		comments = comments[1:]
	}

	// Copy declaration slice, rewriting the ExampleX function to main.
	var decls []ast.Decl
	for _, d := range file.Decls {
		if f, ok := d.(*ast.FuncDecl); ok && isTest(f.Name.Name, "Example") {
			// Copy the FuncDecl, as it may be used elsewhere.
			newF := *f
			newF.Name = ast.NewIdent("main")
			newF.Body, comments = stripOutputComment(f.Body, comments)
			d = &newF
		}
		decls = append(decls, d)
	}

	// Copy the File, as it may be used elsewhere.
	f := *file
	f.Name = ast.NewIdent("main")
	f.Decls = decls
	f.Comments = comments
	return &f
}

// stripOutputComment finds and removes an "Output:" commment from body
// and comments, and adjusts the body block's end position.
func stripOutputComment(body *ast.BlockStmt, comments []*ast.CommentGroup) (*ast.BlockStmt, []*ast.CommentGroup) {
	// Do nothing if no "Output:" comment found.
	i, last := lastComment(body, comments)
	if last == nil || !outputPrefix.MatchString(last.Text()) {
		return body, comments
	}

	// Copy body and comments, as the originals may be used elsewhere.
	newBody := &ast.BlockStmt{
		Lbrace: body.Lbrace,
		List:   body.List,
		Rbrace: last.Pos(),
	}
	newComments := make([]*ast.CommentGroup, len(comments)-1)
	copy(newComments, comments[:i])
	copy(newComments[i:], comments[i+1:])
	return newBody, newComments
}

// lastComment returns the last comment inside the provided block.
func lastComment(b *ast.BlockStmt, c []*ast.CommentGroup) (i int, last *ast.CommentGroup) {
	pos, end := b.Pos(), b.End()
	for j, cg := range c {
		if cg.Pos() < pos {
			continue
		}
		if cg.End() > end {
			break
		}
		i, last = j, cg
	}
	return
}
<commit_msg>go/doc: trim only first space or newline from example output comment<commit_after>// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Extract example functions from file ASTs.

package doc

import (
	"go/ast"
	"go/token"
	"path"
	"regexp"
	"sort"
	"strconv"
	"strings"
	"unicode"
	"unicode/utf8"
)

type Example struct {
	Name     string // name of the item being exemplified
	Doc      string // example function doc string
	Code     ast.Node
	Play     *ast.File // a whole program version of the example
	Comments []*ast.CommentGroup
	Output   string // expected output
}

func Examples(files ...*ast.File) []*Example {
	var list []*Example
	for _, file := range files {
		hasTests := false // file contains tests or benchmarks
		numDecl := 0      // number of non-import declarations in the file
		var flist []*Example
		for _, decl := range file.Decls {
			if g, ok := decl.(*ast.GenDecl); ok && g.Tok != token.IMPORT {
				numDecl++
				continue
			}
			f, ok := decl.(*ast.FuncDecl)
			if !ok {
				continue
			}
			numDecl++
			name := f.Name.Name
			if isTest(name, "Test") || isTest(name, "Benchmark") {
				hasTests = true
				continue
			}
			if !isTest(name, "Example") {
				continue
			}
			var doc string
			if f.Doc != nil {
				doc = f.Doc.Text()
			}
			flist = append(flist, &Example{
				Name:     name[len("Example"):],
				Doc:      doc,
				Code:     f.Body,
				Play:     playExample(file, f.Body),
				Comments: file.Comments,
				Output:   exampleOutput(f.Body, file.Comments),
			})
		}
		if !hasTests && numDecl > 1 && len(flist) == 1 {
			// If this file only has one example function, some
			// other top-level declarations, and no tests or
			// benchmarks, use the whole file as the example.
			flist[0].Code = file
			flist[0].Play = playExampleFile(file)
		}
		list = append(list, flist...)
	}
	sort.Sort(exampleByName(list))
	return list
}

var outputPrefix = regexp.MustCompile(`(?i)^[[:space:]]*output:`)

func exampleOutput(b *ast.BlockStmt, comments []*ast.CommentGroup) string {
	if _, last := lastComment(b, comments); last != nil {
		// test that it begins with the correct prefix
		text := last.Text()
		if loc := outputPrefix.FindStringIndex(text); loc != nil {
			text = text[loc[1]:]
			// Strip zero or more spaces followed by \n or a single space.
			text = strings.TrimLeft(text, " ")
			if len(text) > 0 && text[0] == '\n' {
				text = text[1:]
			}
			return text
		}
	}
	return "" // no suitable comment found
}

// isTest tells whether name looks like a test, example, or benchmark.
// It is a Test (say) if there is a character after Test that is not a
// lower-case letter. (We don't want Testiness.)
func isTest(name, prefix string) bool {
	if !strings.HasPrefix(name, prefix) {
		return false
	}
	if len(name) == len(prefix) { // "Test" is ok
		return true
	}
	rune, _ := utf8.DecodeRuneInString(name[len(prefix):])
	return !unicode.IsLower(rune)
}

type exampleByName []*Example

func (s exampleByName) Len() int           { return len(s) }
func (s exampleByName) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }
func (s exampleByName) Less(i, j int) bool { return s[i].Name < s[j].Name }

// playExample synthesizes a new *ast.File based on the provided
// file with the provided function body as the body of main.
func playExample(file *ast.File, body *ast.BlockStmt) *ast.File {
	if !strings.HasSuffix(file.Name.Name, "_test") {
		// We don't support examples that are part of the
		// greater package (yet).
		return nil
	}

	// Find top-level declarations in the file.
	topDecls := make(map[*ast.Object]bool)
	for _, decl := range file.Decls {
		switch d := decl.(type) {
		case *ast.FuncDecl:
			topDecls[d.Name.Obj] = true
		case *ast.GenDecl:
			for _, spec := range d.Specs {
				switch s := spec.(type) {
				case *ast.TypeSpec:
					topDecls[s.Name.Obj] = true
				case *ast.ValueSpec:
					for _, id := range s.Names {
						topDecls[id.Obj] = true
					}
				}
			}
		}
	}

	// Find unresolved identifiers and uses of top-level declarations.
	unresolved := make(map[string]bool)
	usesTopDecl := false
	ast.Inspect(body, func(n ast.Node) bool {
		// For an expression like fmt.Println, only add "fmt" to the
		// set of unresolved names.
		if e, ok := n.(*ast.SelectorExpr); ok {
			if id, ok := e.X.(*ast.Ident); ok && id.Obj == nil {
				unresolved[id.Name] = true
			}
			return false
		}
		if id, ok := n.(*ast.Ident); ok {
			if id.Obj == nil {
				unresolved[id.Name] = true
			} else if topDecls[id.Obj] {
				usesTopDecl = true
			}
		}
		return true
	})
	if usesTopDecl {
		// We don't support examples that are not self-contained (yet).
		return nil
	}

	// Remove predeclared identifiers from unresolved list.
	for n := range unresolved {
		if predeclaredTypes[n] || predeclaredConstants[n] || predeclaredFuncs[n] {
			delete(unresolved, n)
		}
	}

	// Use unresolved identifiers to determine the imports used by this
	// example. The heuristic assumes package names match base import
	// paths for imports w/o renames (should be good enough most of the time).
	namedImports := make(map[string]string) // [name]path
	var blankImports []ast.Spec             // _ imports
	for _, s := range file.Imports {
		p, err := strconv.Unquote(s.Path.Value)
		if err != nil {
			continue
		}
		n := path.Base(p)
		if s.Name != nil {
			n = s.Name.Name
			switch n {
			case "_":
				blankImports = append(blankImports, s)
				continue
			case ".":
				// We can't resolve dot imports (yet).
				return nil
			}
		}
		if unresolved[n] {
			namedImports[n] = p
			delete(unresolved, n)
		}
	}

	// If there are other unresolved identifiers, give up because this
	// synthesized file is not going to build.
	if len(unresolved) > 0 {
		return nil
	}

	// Include documentation belonging to blank imports.
	var comments []*ast.CommentGroup
	for _, s := range blankImports {
		if c := s.(*ast.ImportSpec).Doc; c != nil {
			comments = append(comments, c)
		}
	}

	// Include comments that are inside the function body.
	for _, c := range file.Comments {
		if body.Pos() <= c.Pos() && c.End() <= body.End() {
			comments = append(comments, c)
		}
	}

	// Strip "Output:" commment and adjust body end position.
	body, comments = stripOutputComment(body, comments)

	// Synthesize import declaration.
	importDecl := &ast.GenDecl{
		Tok:    token.IMPORT,
		Lparen: 1, // Need non-zero Lparen and Rparen so that printer
		Rparen: 1, // treats this as a factored import.
	}
	for n, p := range namedImports {
		s := &ast.ImportSpec{Path: &ast.BasicLit{Value: strconv.Quote(p)}}
		if path.Base(p) != n {
			s.Name = ast.NewIdent(n)
		}
		importDecl.Specs = append(importDecl.Specs, s)
	}
	importDecl.Specs = append(importDecl.Specs, blankImports...)

	// Synthesize main function.
	funcDecl := &ast.FuncDecl{
		Name: ast.NewIdent("main"),
		Type: &ast.FuncType{},
		Body: body,
	}

	// Synthesize file.
	return &ast.File{
		Name:     ast.NewIdent("main"),
		Decls:    []ast.Decl{importDecl, funcDecl},
		Comments: comments,
	}
}

// playExampleFile takes a whole file example and synthesizes a new *ast.File
// such that the example is function main in package main.
func playExampleFile(file *ast.File) *ast.File {
	// Strip copyright comment if present.
	comments := file.Comments
	if len(comments) > 0 && strings.HasPrefix(comments[0].Text(), "Copyright") {
		comments = comments[1:]
	}

	// Copy declaration slice, rewriting the ExampleX function to main.
	var decls []ast.Decl
	for _, d := range file.Decls {
		if f, ok := d.(*ast.FuncDecl); ok && isTest(f.Name.Name, "Example") {
			// Copy the FuncDecl, as it may be used elsewhere.
			newF := *f
			newF.Name = ast.NewIdent("main")
			newF.Body, comments = stripOutputComment(f.Body, comments)
			d = &newF
		}
		decls = append(decls, d)
	}

	// Copy the File, as it may be used elsewhere.
	f := *file
	f.Name = ast.NewIdent("main")
	f.Decls = decls
	f.Comments = comments
	return &f
}

// stripOutputComment finds and removes an "Output:" commment from body
// and comments, and adjusts the body block's end position.
func stripOutputComment(body *ast.BlockStmt, comments []*ast.CommentGroup) (*ast.BlockStmt, []*ast.CommentGroup) {
	// Do nothing if no "Output:" comment found.
	i, last := lastComment(body, comments)
	if last == nil || !outputPrefix.MatchString(last.Text()) {
		return body, comments
	}

	// Copy body and comments, as the originals may be used elsewhere.
	newBody := &ast.BlockStmt{
		Lbrace: body.Lbrace,
		List:   body.List,
		Rbrace: last.Pos(),
	}
	newComments := make([]*ast.CommentGroup, len(comments)-1)
	copy(newComments, comments[:i])
	copy(newComments[i:], comments[i+1:])
	return newBody, newComments
}

// lastComment returns the last comment inside the provided block.
func lastComment(b *ast.BlockStmt, c []*ast.CommentGroup) (i int, last *ast.CommentGroup) {
	pos, end := b.Pos(), b.End()
	for j, cg := range c {
		if cg.Pos() < pos {
			continue
		}
		if cg.End() > end {
			break
		}
		i, last = j, cg
	}
	return
}
<|endoftext|>


<commit_before>// Package watchfiles provides a reusable utility to watch files and get notified of changes
package watchfiles

import (
	"bufio"
	"log"
	"os"
	"path/filepath"
	"regexp"
	"sync"
	"time"

	"gopkg.in/fsnotify.v1"
)

var logger = log.New(os.Stderr, "watchfiles: ", log.LstdFlags|log.Lmicroseconds)

// SetLog allows customizing the logger
func SetLog(l *log.Logger) {
	logger = l
}

// WatchFiles allows loading, and optionally watching files for changes.
type WatchFiles struct {
	path        string
	filePattern *regexp.Regexp
	updateCb    UpdateCallback
	removeCb    RemoveCallback

	eventsLock *sync.Mutex
	eventsMap  map[string]fsnotify.Event
	stop       chan bool
}

// UpdateCallback gets invoked when a watched file is modified,
// or when a new file is added to a watched directory
type UpdateCallback func(filePath string, reader *bufio.Reader)

// RemoveCallback gets invoked when a watched file is modified
type RemoveCallback func(filePath string)

// NewWatchFiles is the constructor function for a Config instance
//
//	The following parameters are supported:
//		filePath: the directory or file to open and watch. If a directory is used, all files in it are loaded
//		updateCb: (optional) a callback that is invoked when config data files are updated, or added
//		removeCb: (optional) a callback that is invoked when config data files are removed
func NewWatchFiles(filePath string, filePattern *regexp.Regexp, updateCb UpdateCallback,
	removeCb RemoveCallback) (*WatchFiles, error) {

	c := WatchFiles{path: filePath, filePattern: filePattern}
	c.updateCb = updateCb
	c.removeCb = removeCb

	c.eventsLock = &sync.Mutex{}
	c.eventsMap = make(map[string]fsnotify.Event)
	return &c, nil
}

// StartWatch loads and watches files
func (wf *WatchFiles) StartWatch() error {
	err := wf.loadFiles(wf.path)
	if err != nil {
		return err
	}

	wf.watchFile(wf.path)
	return nil
}

// StopWatch stops watching files
func (wf *WatchFiles) StopWatch() {
	wf.stop <- true
}

func (wf *WatchFiles) loadFiles(filePath string) error {
	f, err := os.Open(filePath)
	if err != nil {
		logger.Printf("ERROR - Unable to open: %s", filePath)
		return err
	}

	defer f.Close()
	fi, err := f.Stat()
	if err != nil {
		logger.Printf("ERROR - Unable to stat: %s", filePath)
		return err
	}

	if fi.IsDir() {
		list, err := f.Readdir(-1)
		if err != nil {
			logger.Printf("ERROR - Unable to read directory: %s", fi.Name())
			return err
		}

		for _, dirFile := range list {
			if dirFile.IsDir() {
				logger.Printf("INFO - Skipping sub-directory %s ", fi.Name())
				continue
			}

			var fileName = filepath.Join(f.Name(), dirFile.Name())
			wf.openAndloadFile(fileName)
		}
	} else {
		wf.loadFile(f)
	}

	return nil
}

func (wf *WatchFiles) openAndloadFile(filePath string) {
	logger.Printf("INFO - Processsing file: %s", filePath)

	configFile, err := os.Open(filePath)
	if err != nil {
		logger.Printf("ERROR - %v", err)
		return
	}

	defer configFile.Close()
	wf.loadFile(configFile)
}

func (wf *WatchFiles) loadFile(configFile *os.File) {
	rdr := bufio.NewReader(configFile)
	logger.Printf("loaded data from: %s", configFile.Name())

	if wf.updateCb != nil {
		wf.updateCb(configFile.Name(), rdr)
	}
}

func (wf *WatchFiles) watchFile(filePath string) {
	watcher, err := fsnotify.NewWatcher()
	if err != nil {
		logger.Printf("ERROR - %v", err)
	}

	go func() {
		for {
			timer := time.NewTimer(1 * time.Second)
			select {
			case event := <-watcher.Events:
				logger.Printf("INFO - Received file watch event: %v", event)

				fn := filepath.Base(event.Name)
				if fn == "" {
					logger.Printf("ERROR - unable to get filename from event: %v", event)
					continue
				}

				if !wf.filePattern.MatchString(fn) {
					logger.Printf("INFO - ignoring file %s as it does not match name pattern", event.Name)
					continue
				}

				wf.storeEvent(event)
				timer.Stop()

			case <-timer.C:
				wf.processEvents()

			case <-wf.stop:
				logger.Printf("Stopping file watch")
				return

			case err := <-watcher.Errors:
				logger.Printf("error: %v from file watcher: %s", err, filePath)
			}
		}
	}()

	err = watcher.Add(filePath)
	if err != nil {
		logger.Printf("ERROR - %v", err)
		return
	}

	logger.Printf("INFO - Watching file: %s", filePath)
}

func (wf *WatchFiles) storeEvent(event fsnotify.Event) {
	wf.eventsLock.Lock()
	defer wf.eventsLock.Unlock()

	wf.eventsMap[event.Name] = event
	logger.Printf("INFO - %d pending events", len(wf.eventsMap))
}

func (wf *WatchFiles) processEvents() {
	wf.eventsLock.Lock()
	defer wf.eventsLock.Unlock()

	if len(wf.eventsMap) == 0 {
		return
	}

	logger.Printf("INFO - Processing %d events", len(wf.eventsMap))

	for _, event := range wf.eventsMap {
		delete(wf.eventsMap, event.Name)
		if ((event.Op & fsnotify.Write) == fsnotify.Write) || ((event.Op & fsnotify.Chmod) == fsnotify.Chmod) {
			logger.Printf("INFO - Modified file: %s", event.Name)
			wf.openAndloadFile(event.Name)
		} else if (event.Op & fsnotify.Create) == fsnotify.Create {
			logger.Printf("INFO - Created file: %s", event.Name)
			wf.openAndloadFile(event.Name)
		} else if (event.Op & fsnotify.Remove) == fsnotify.Remove {
			logger.Printf("INFO - Deleted file: %s", event.Name)
			if wf.removeCb != nil {
				wf.removeCb(event.Name)
			}
		} else if (event.Op & fsnotify.Rename) == fsnotify.Rename {
			logger.Printf("INFO - Renamed file: %s", event.Name)
			wf.openAndloadFile(event.Name)
		}
	}
}
<commit_msg>turn off logger by default<commit_after>// Package watchfiles provides a reusable utility to watch files and get notified of changes
package watchfiles

import (
	"bufio"
	"log"
	"os"
	"path/filepath"
	"regexp"
	"sync"
	"time"

	"io/ioutil"

	"gopkg.in/fsnotify.v1"
)

// var logger = log.New(os.Stderr, "watchfiles: ", log.LstdFlags|log.Lmicroseconds)
var logger = log.New(ioutil.Discard, "watchfiles: ", log.LstdFlags|log.Lmicroseconds)

// SetLog allows customizing the logger
func SetLog(l *log.Logger) {
	logger = l
}

// WatchFiles allows loading, and optionally watching files for changes.
type WatchFiles struct {
	path        string
	filePattern *regexp.Regexp
	updateCb    UpdateCallback
	removeCb    RemoveCallback

	eventsLock *sync.Mutex
	eventsMap  map[string]fsnotify.Event
	stop       chan bool
}

// UpdateCallback gets invoked when a watched file is modified,
// or when a new file is added to a watched directory
type UpdateCallback func(filePath string, reader *bufio.Reader)

// RemoveCallback gets invoked when a watched file is modified
type RemoveCallback func(filePath string)

// NewWatchFiles is the constructor function for a Config instance
//
//	The following parameters are supported:
//		filePath: the directory or file to open and watch. If a directory is used, all files in it are loaded
//		updateCb: (optional) a callback that is invoked when config data files are updated, or added
//		removeCb: (optional) a callback that is invoked when config data files are removed
func NewWatchFiles(filePath string, filePattern *regexp.Regexp, updateCb UpdateCallback,
	removeCb RemoveCallback) (*WatchFiles, error) {

	c := WatchFiles{path: filePath, filePattern: filePattern}
	c.updateCb = updateCb
	c.removeCb = removeCb

	c.eventsLock = &sync.Mutex{}
	c.eventsMap = make(map[string]fsnotify.Event)
	return &c, nil
}

// StartWatch loads and watches files
func (wf *WatchFiles) StartWatch() error {
	err := wf.loadFiles(wf.path)
	if err != nil {
		return err
	}

	wf.watchFile(wf.path)
	return nil
}

// StopWatch stops watching files
func (wf *WatchFiles) StopWatch() {
	wf.stop <- true
}

func (wf *WatchFiles) loadFiles(filePath string) error {
	f, err := os.Open(filePath)
	if err != nil {
		logger.Printf("ERROR - Unable to open: %s", filePath)
		return err
	}

	defer f.Close()
	fi, err := f.Stat()
	if err != nil {
		logger.Printf("ERROR - Unable to stat: %s", filePath)
		return err
	}

	if fi.IsDir() {
		list, err := f.Readdir(-1)
		if err != nil {
			logger.Printf("ERROR - Unable to read directory: %s", fi.Name())
			return err
		}

		for _, dirFile := range list {
			if dirFile.IsDir() {
				logger.Printf("INFO - Skipping sub-directory %s ", fi.Name())
				continue
			}

			var fileName = filepath.Join(f.Name(), dirFile.Name())
			wf.openAndloadFile(fileName)
		}
	} else {
		wf.loadFile(f)
	}

	return nil
}

func (wf *WatchFiles) openAndloadFile(filePath string) {
	logger.Printf("INFO - Processsing file: %s", filePath)

	configFile, err := os.Open(filePath)
	if err != nil {
		logger.Printf("ERROR - %v", err)
		return
	}

	defer configFile.Close()
	wf.loadFile(configFile)
}

func (wf *WatchFiles) loadFile(configFile *os.File) {
	rdr := bufio.NewReader(configFile)
	logger.Printf("loaded data from: %s", configFile.Name())

	if wf.updateCb != nil {
		wf.updateCb(configFile.Name(), rdr)
	}
}

func (wf *WatchFiles) watchFile(filePath string) {
	watcher, err := fsnotify.NewWatcher()
	if err != nil {
		logger.Printf("ERROR - %v", err)
	}

	go func() {
		for {
			timer := time.NewTimer(1 * time.Second)
			select {
			case event := <-watcher.Events:
				logger.Printf("INFO - Received file watch event: %v", event)

				fn := filepath.Base(event.Name)
				if fn == "" {
					logger.Printf("ERROR - unable to get filename from event: %v", event)
					continue
				}

				if !wf.filePattern.MatchString(fn) {
					logger.Printf("INFO - ignoring file %s as it does not match name pattern", event.Name)
					continue
				}

				wf.storeEvent(event)
				timer.Stop()

			case <-timer.C:
				wf.processEvents()

			case <-wf.stop:
				logger.Printf("Stopping file watch")
				return

			case err := <-watcher.Errors:
				logger.Printf("error: %v from file watcher: %s", err, filePath)
			}
		}
	}()

	err = watcher.Add(filePath)
	if err != nil {
		logger.Printf("ERROR - %v", err)
		return
	}

	logger.Printf("INFO - Watching file: %s", filePath)
}

func (wf *WatchFiles) storeEvent(event fsnotify.Event) {
	wf.eventsLock.Lock()
	defer wf.eventsLock.Unlock()

	wf.eventsMap[event.Name] = event
	logger.Printf("INFO - %d pending events", len(wf.eventsMap))
}

func (wf *WatchFiles) processEvents() {
	wf.eventsLock.Lock()
	defer wf.eventsLock.Unlock()

	if len(wf.eventsMap) == 0 {
		return
	}

	logger.Printf("INFO - Processing %d events", len(wf.eventsMap))

	for _, event := range wf.eventsMap {
		delete(wf.eventsMap, event.Name)
		if ((event.Op & fsnotify.Write) == fsnotify.Write) || ((event.Op & fsnotify.Chmod) == fsnotify.Chmod) {
			logger.Printf("INFO - Modified file: %s", event.Name)
			wf.openAndloadFile(event.Name)
		} else if (event.Op & fsnotify.Create) == fsnotify.Create {
			logger.Printf("INFO - Created file: %s", event.Name)
			wf.openAndloadFile(event.Name)
		} else if (event.Op & fsnotify.Remove) == fsnotify.Remove {
			logger.Printf("INFO - Deleted file: %s", event.Name)
			if wf.removeCb != nil {
				wf.removeCb(event.Name)
			}
		} else if (event.Op & fsnotify.Rename) == fsnotify.Rename {
			logger.Printf("INFO - Renamed file: %s", event.Name)
			wf.openAndloadFile(event.Name)
		}
	}
}
<|endoftext|>