port some useful diff utils
This commit is contained in:
		
							parent
							
								
									ada76c11a9
								
							
						
					
					
						commit
						3d4f816787
					
				|  | @ -217,9 +217,15 @@ | |||
|   packages = ["pkg/common"] | ||||
|   revision = "39a7bf85c140f972372c2a0d1ee40adbf0c8bfe1" | ||||
| 
 | ||||
| [[projects]] | ||||
|   branch = "master" | ||||
|   name = "k8s.io/utils" | ||||
|   packages = ["exec"] | ||||
|   revision = "258e2a2fa64568210fbd6267cf1d8fd87c3cb86e" | ||||
| 
 | ||||
| [solve-meta] | ||||
|   analyzer-name = "dep" | ||||
|   analyzer-version = 1 | ||||
|   inputs-digest = "2b20626f5c5afea676ac5585d24de154de000f6a9fb0a307eb264a8cf3bd0253" | ||||
|   inputs-digest = "a778052416a71e5aca31256e71794eda11fed3c896b231ca36a045770abad827" | ||||
|   solver-name = "gps-cdcl" | ||||
|   solver-version = 1 | ||||
|  |  | |||
|  | @ -36,3 +36,7 @@ | |||
| [[constraint]] | ||||
|   branch = "master" | ||||
|   name = "k8s.io/client-go" | ||||
| 
 | ||||
| [[constraint]] | ||||
|   branch = "master" | ||||
|   name = "k8s.io/utils" | ||||
|  |  | |||
|  | @ -0,0 +1,104 @@ | |||
| /* | ||||
| Copyright 2018 The Kubernetes Authors. | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
| 
 | ||||
|     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| 
 | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| package util | ||||
| 
 | ||||
| import ( | ||||
| 	"io" | ||||
| 	"io/ioutil" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 
 | ||||
| 	"github.com/ghodss/yaml" | ||||
| 
 | ||||
| 	"k8s.io/utils/exec" | ||||
| ) | ||||
| 
 | ||||
| // DiffProgram finds and run the diff program. The value of
 | ||||
| // KUBERNETES_EXTERNAL_DIFF environment variable will be used a diff
 | ||||
| // program. By default, `diff(1)` will be used.
 | ||||
| type DiffProgram struct { | ||||
| 	Exec   exec.Interface | ||||
| 	Stdout io.Writer | ||||
| 	Stderr io.Writer | ||||
| } | ||||
| 
 | ||||
| func (d *DiffProgram) getCommand(args ...string) exec.Cmd { | ||||
| 	diff := "" | ||||
| 	if envDiff := os.Getenv("KUBERNETES_EXTERNAL_DIFF"); envDiff != "" { | ||||
| 		diff = envDiff | ||||
| 	} else { | ||||
| 		diff = "diff" | ||||
| 		args = append([]string{"-u", "-N"}, args...) | ||||
| 	} | ||||
| 
 | ||||
| 	cmd := d.Exec.Command(diff, args...) | ||||
| 	cmd.SetStdout(d.Stdout) | ||||
| 	cmd.SetStderr(d.Stderr) | ||||
| 
 | ||||
| 	return cmd | ||||
| } | ||||
| 
 | ||||
| // Run runs the detected diff program. `from` and `to` are the directory to diff.
 | ||||
| func (d *DiffProgram) Run(from, to string) error { | ||||
| 	d.getCommand(from, to).Run() // Ignore diff return code
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // Printer is used to print an object.
 | ||||
| type Printer struct{} | ||||
| 
 | ||||
| // Print the object inside the writer w.
 | ||||
| func (p *Printer) Print(obj interface{}, w io.Writer) error { | ||||
| 	if obj == nil { | ||||
| 		return nil | ||||
| 	} | ||||
| 	data, err := yaml.Marshal(obj) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	_, err = w.Write(data) | ||||
| 	return err | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| // Directory creates a new temp directory, and allows to easily create new files.
 | ||||
| type Directory struct { | ||||
| 	Name string | ||||
| } | ||||
| 
 | ||||
| // CreateDirectory does create the actual disk directory, and return a
 | ||||
| // new representation of it.
 | ||||
| func CreateDirectory(prefix string) (*Directory, error) { | ||||
| 	name, err := ioutil.TempDir("", prefix+"-") | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return &Directory{ | ||||
| 		Name: name, | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| // NewFile creates a new file in the directory.
 | ||||
| func (d *Directory) NewFile(name string) (*os.File, error) { | ||||
| 	return os.OpenFile(filepath.Join(d.Name, name), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0700) | ||||
| } | ||||
| 
 | ||||
| // Delete removes the directory recursively.
 | ||||
| func (d *Directory) Delete() error { | ||||
| 	return os.RemoveAll(d.Name) | ||||
| } | ||||
|  | @ -0,0 +1,9 @@ | |||
| language: go | ||||
| go: | ||||
|   - 1.8.x | ||||
|   - 1.9.x | ||||
| go_import_path: k8s.io/utils | ||||
| script: | ||||
|   - diff -u <(echo -n) <(gofmt -d .) | ||||
|   - go tool vet . | ||||
|   - go test -v -race ./... | ||||
|  | @ -0,0 +1,31 @@ | |||
| # How to move a utility pkg from other kubernetes repos | ||||
| 
 | ||||
| It has 2 steps to move a pkg from other Kubernetes repos to `k8s.io/utils` repo: | ||||
| - copy the pkg to `k8s.io/utils` repo | ||||
| - update the import paths and `vendor/` in the repos that refer this pkg | ||||
| 
 | ||||
| ## Copy the pkg to `k8s.io/utils` repo | ||||
| 
 | ||||
| Copying should preserve all the git history associated with it. | ||||
| [Here](http://gbayer.com/development/moving-files-from-one-git-repository-to-another-preserving-history/) is a working approach. | ||||
| Note: You may need to use `--allow-unrelated-histories` if you get error when running `git pull` following the post above. | ||||
| 
 | ||||
| Then, you may need to restructure the package to make sure it has the following structure. | ||||
| 
 | ||||
|     . | ||||
|     ├── doc.go                  # Description for this package | ||||
|     ├── <utilname1>.go          # utility go file | ||||
|     ├── <utilname>_test.go      # go unit tests | ||||
|     └── testing                 # All the testing framework | ||||
|         └── fake_<utilname>.go  # Testing framework go file | ||||
| 
 | ||||
| [#5](https://github.com/kubernetes/utils/pull/5) is an example for this step. | ||||
| 
 | ||||
| ## Update the repos that refer the pkg | ||||
| 
 | ||||
| You should update the import paths. | ||||
| Then follow [this doc](https://github.com/kubernetes/community/blob/master/contributors/devel/godep.md) to update `vendor/` and `Godeps/`. | ||||
| 
 | ||||
| You may want to run `make bazel-test` to make sure all new references work. | ||||
| 
 | ||||
| [kubernetes/kubernetes#49234](https://github.com/kubernetes/kubernetes/pull/49234) is an example for this step. | ||||
|  | @ -0,0 +1,202 @@ | |||
| 
 | ||||
|                                  Apache License | ||||
|                            Version 2.0, January 2004 | ||||
|                         http://www.apache.org/licenses/ | ||||
| 
 | ||||
|    TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | ||||
| 
 | ||||
|    1. Definitions. | ||||
| 
 | ||||
|       "License" shall mean the terms and conditions for use, reproduction, | ||||
|       and distribution as defined by Sections 1 through 9 of this document. | ||||
| 
 | ||||
|       "Licensor" shall mean the copyright owner or entity authorized by | ||||
|       the copyright owner that is granting the License. | ||||
| 
 | ||||
|       "Legal Entity" shall mean the union of the acting entity and all | ||||
|       other entities that control, are controlled by, or are under common | ||||
|       control with that entity. For the purposes of this definition, | ||||
|       "control" means (i) the power, direct or indirect, to cause the | ||||
|       direction or management of such entity, whether by contract or | ||||
|       otherwise, or (ii) ownership of fifty percent (50%) or more of the | ||||
|       outstanding shares, or (iii) beneficial ownership of such entity. | ||||
| 
 | ||||
|       "You" (or "Your") shall mean an individual or Legal Entity | ||||
|       exercising permissions granted by this License. | ||||
| 
 | ||||
|       "Source" form shall mean the preferred form for making modifications, | ||||
|       including but not limited to software source code, documentation | ||||
|       source, and configuration files. | ||||
| 
 | ||||
|       "Object" form shall mean any form resulting from mechanical | ||||
|       transformation or translation of a Source form, including but | ||||
|       not limited to compiled object code, generated documentation, | ||||
|       and conversions to other media types. | ||||
| 
 | ||||
|       "Work" shall mean the work of authorship, whether in Source or | ||||
|       Object form, made available under the License, as indicated by a | ||||
|       copyright notice that is included in or attached to the work | ||||
|       (an example is provided in the Appendix below). | ||||
| 
 | ||||
|       "Derivative Works" shall mean any work, whether in Source or Object | ||||
|       form, that is based on (or derived from) the Work and for which the | ||||
|       editorial revisions, annotations, elaborations, or other modifications | ||||
|       represent, as a whole, an original work of authorship. For the purposes | ||||
|       of this License, Derivative Works shall not include works that remain | ||||
|       separable from, or merely link (or bind by name) to the interfaces of, | ||||
|       the Work and Derivative Works thereof. | ||||
| 
 | ||||
|       "Contribution" shall mean any work of authorship, including | ||||
|       the original version of the Work and any modifications or additions | ||||
|       to that Work or Derivative Works thereof, that is intentionally | ||||
|       submitted to Licensor for inclusion in the Work by the copyright owner | ||||
|       or by an individual or Legal Entity authorized to submit on behalf of | ||||
|       the copyright owner. For the purposes of this definition, "submitted" | ||||
|       means any form of electronic, verbal, or written communication sent | ||||
|       to the Licensor or its representatives, including but not limited to | ||||
|       communication on electronic mailing lists, source code control systems, | ||||
|       and issue tracking systems that are managed by, or on behalf of, the | ||||
|       Licensor for the purpose of discussing and improving the Work, but | ||||
|       excluding communication that is conspicuously marked or otherwise | ||||
|       designated in writing by the copyright owner as "Not a Contribution." | ||||
| 
 | ||||
|       "Contributor" shall mean Licensor and any individual or Legal Entity | ||||
|       on behalf of whom a Contribution has been received by Licensor and | ||||
|       subsequently incorporated within the Work. | ||||
| 
 | ||||
|    2. Grant of Copyright License. Subject to the terms and conditions of | ||||
|       this License, each Contributor hereby grants to You a perpetual, | ||||
|       worldwide, non-exclusive, no-charge, royalty-free, irrevocable | ||||
|       copyright license to reproduce, prepare Derivative Works of, | ||||
|       publicly display, publicly perform, sublicense, and distribute the | ||||
|       Work and such Derivative Works in Source or Object form. | ||||
| 
 | ||||
|    3. Grant of Patent License. Subject to the terms and conditions of | ||||
|       this License, each Contributor hereby grants to You a perpetual, | ||||
|       worldwide, non-exclusive, no-charge, royalty-free, irrevocable | ||||
|       (except as stated in this section) patent license to make, have made, | ||||
|       use, offer to sell, sell, import, and otherwise transfer the Work, | ||||
|       where such license applies only to those patent claims licensable | ||||
|       by such Contributor that are necessarily infringed by their | ||||
|       Contribution(s) alone or by combination of their Contribution(s) | ||||
|       with the Work to which such Contribution(s) was submitted. If You | ||||
|       institute patent litigation against any entity (including a | ||||
|       cross-claim or counterclaim in a lawsuit) alleging that the Work | ||||
|       or a Contribution incorporated within the Work constitutes direct | ||||
|       or contributory patent infringement, then any patent licenses | ||||
|       granted to You under this License for that Work shall terminate | ||||
|       as of the date such litigation is filed. | ||||
| 
 | ||||
|    4. Redistribution. You may reproduce and distribute copies of the | ||||
|       Work or Derivative Works thereof in any medium, with or without | ||||
|       modifications, and in Source or Object form, provided that You | ||||
|       meet the following conditions: | ||||
| 
 | ||||
|       (a) You must give any other recipients of the Work or | ||||
|           Derivative Works a copy of this License; and | ||||
| 
 | ||||
|       (b) You must cause any modified files to carry prominent notices | ||||
|           stating that You changed the files; and | ||||
| 
 | ||||
|       (c) You must retain, in the Source form of any Derivative Works | ||||
|           that You distribute, all copyright, patent, trademark, and | ||||
|           attribution notices from the Source form of the Work, | ||||
|           excluding those notices that do not pertain to any part of | ||||
|           the Derivative Works; and | ||||
| 
 | ||||
|       (d) If the Work includes a "NOTICE" text file as part of its | ||||
|           distribution, then any Derivative Works that You distribute must | ||||
|           include a readable copy of the attribution notices contained | ||||
|           within such NOTICE file, excluding those notices that do not | ||||
|           pertain to any part of the Derivative Works, in at least one | ||||
|           of the following places: within a NOTICE text file distributed | ||||
|           as part of the Derivative Works; within the Source form or | ||||
|           documentation, if provided along with the Derivative Works; or, | ||||
|           within a display generated by the Derivative Works, if and | ||||
|           wherever such third-party notices normally appear. The contents | ||||
|           of the NOTICE file are for informational purposes only and | ||||
|           do not modify the License. You may add Your own attribution | ||||
|           notices within Derivative Works that You distribute, alongside | ||||
|           or as an addendum to the NOTICE text from the Work, provided | ||||
|           that such additional attribution notices cannot be construed | ||||
|           as modifying the License. | ||||
| 
 | ||||
|       You may add Your own copyright statement to Your modifications and | ||||
|       may provide additional or different license terms and conditions | ||||
|       for use, reproduction, or distribution of Your modifications, or | ||||
|       for any such Derivative Works as a whole, provided Your use, | ||||
|       reproduction, and distribution of the Work otherwise complies with | ||||
|       the conditions stated in this License. | ||||
| 
 | ||||
|    5. Submission of Contributions. Unless You explicitly state otherwise, | ||||
|       any Contribution intentionally submitted for inclusion in the Work | ||||
|       by You to the Licensor shall be under the terms and conditions of | ||||
|       this License, without any additional terms or conditions. | ||||
|       Notwithstanding the above, nothing herein shall supersede or modify | ||||
|       the terms of any separate license agreement you may have executed | ||||
|       with Licensor regarding such Contributions. | ||||
| 
 | ||||
|    6. Trademarks. This License does not grant permission to use the trade | ||||
|       names, trademarks, service marks, or product names of the Licensor, | ||||
|       except as required for reasonable and customary use in describing the | ||||
|       origin of the Work and reproducing the content of the NOTICE file. | ||||
| 
 | ||||
|    7. Disclaimer of Warranty. Unless required by applicable law or | ||||
|       agreed to in writing, Licensor provides the Work (and each | ||||
|       Contributor provides its Contributions) on an "AS IS" BASIS, | ||||
|       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | ||||
|       implied, including, without limitation, any warranties or conditions | ||||
|       of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | ||||
|       PARTICULAR PURPOSE. You are solely responsible for determining the | ||||
|       appropriateness of using or redistributing the Work and assume any | ||||
|       risks associated with Your exercise of permissions under this License. | ||||
| 
 | ||||
|    8. Limitation of Liability. In no event and under no legal theory, | ||||
|       whether in tort (including negligence), contract, or otherwise, | ||||
|       unless required by applicable law (such as deliberate and grossly | ||||
|       negligent acts) or agreed to in writing, shall any Contributor be | ||||
|       liable to You for damages, including any direct, indirect, special, | ||||
|       incidental, or consequential damages of any character arising as a | ||||
|       result of this License or out of the use or inability to use the | ||||
|       Work (including but not limited to damages for loss of goodwill, | ||||
|       work stoppage, computer failure or malfunction, or any and all | ||||
|       other commercial damages or losses), even if such Contributor | ||||
|       has been advised of the possibility of such damages. | ||||
| 
 | ||||
|    9. Accepting Warranty or Additional Liability. While redistributing | ||||
|       the Work or Derivative Works thereof, You may choose to offer, | ||||
|       and charge a fee for, acceptance of support, warranty, indemnity, | ||||
|       or other liability obligations and/or rights consistent with this | ||||
|       License. However, in accepting such obligations, You may act only | ||||
|       on Your own behalf and on Your sole responsibility, not on behalf | ||||
|       of any other Contributor, and only if You agree to indemnify, | ||||
|       defend, and hold each Contributor harmless for any liability | ||||
|       incurred by, or claims asserted against, such Contributor by reason | ||||
|       of your accepting any such warranty or additional liability. | ||||
| 
 | ||||
|    END OF TERMS AND CONDITIONS | ||||
| 
 | ||||
|    APPENDIX: How to apply the Apache License to your work. | ||||
| 
 | ||||
|       To apply the Apache License to your work, attach the following | ||||
|       boilerplate notice, with the fields enclosed by brackets "[]" | ||||
|       replaced with your own identifying information. (Don't include | ||||
|       the brackets!)  The text should be enclosed in the appropriate | ||||
|       comment syntax for the file format. We also recommend that a | ||||
|       file or class name and description of purpose be included on the | ||||
|       same "printed page" as the copyright notice for easier | ||||
|       identification within third-party archives. | ||||
| 
 | ||||
|    Copyright [yyyy] [name of copyright owner] | ||||
| 
 | ||||
|    Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|    you may not use this file except in compliance with the License. | ||||
|    You may obtain a copy of the License at | ||||
| 
 | ||||
|        http://www.apache.org/licenses/LICENSE-2.0 | ||||
| 
 | ||||
|    Unless required by applicable law or agreed to in writing, software | ||||
|    distributed under the License is distributed on an "AS IS" BASIS, | ||||
|    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|    See the License for the specific language governing permissions and | ||||
|    limitations under the License. | ||||
|  | @ -0,0 +1,57 @@ | |||
| # Utils | ||||
| 
 | ||||
| [![Build Status]](https://travis-ci.org/kubernetes/utils) [](https://godoc.org/k8s.io/utils) | ||||
| 
 | ||||
| A set of Go libraries that provide low-level, | ||||
| kubernetes-independent packages supplementing the [Go | ||||
| standard libs]. | ||||
| 
 | ||||
| ## Purpose | ||||
| 
 | ||||
| As Kubernetes grows and spins functionality out of its | ||||
| [core] and into cooperating repositories like | ||||
| [apiserver], [kubectl], [kubeadm], etc., the need | ||||
| arises for leaf repositories to house shared code and | ||||
| avoid cycles in repository relationships. | ||||
| 
 | ||||
| This repository is intended to hold shared utilities | ||||
| with no Kubernetes dependence that may be of interest | ||||
| to any Go project.  See these [instructions for moving] | ||||
| an existing package to this repository. | ||||
| 
 | ||||
| 
 | ||||
| ## Criteria for adding code here | ||||
| 
 | ||||
| - Used by multiple Kubernetes repositories. | ||||
| 
 | ||||
| - Full unit test coverage. | ||||
| 
 | ||||
| - Go tools compliant (`go get`, `go test`, etc.). | ||||
| 
 | ||||
| - Complex enough to be worth vendoring, rather than copying. | ||||
| 
 | ||||
| - Stable, or backward compatible, API. | ||||
| 
 | ||||
| - _No dependence on any Kubernetes repository_. | ||||
| 
 | ||||
| ## Libraries | ||||
| 
 | ||||
| - [Exec](/exec) provides an interface for `os/exec`. It makes it easier | ||||
|   to mock and replace in tests, especially with | ||||
|   the [FakeExec](exec/testing/fake_exec.go) struct. | ||||
| 
 | ||||
| - [Temp](/temp) provides an interface to create temporary directories. It also | ||||
|   provides a [FakeDir](temp/temptest) implementation to replace in tests. | ||||
| 
 | ||||
| - [Clock](/clock) provides an interface for time-based operations.  It allows | ||||
|   mocking time for testing. | ||||
| 
 | ||||
| [Build Status]: https://travis-ci.org/kubernetes/utils.svg?branch=master | ||||
| [Go standard libs]: https://golang.org/pkg/#stdlib | ||||
| [api]: https://github.com/kubernetes/api | ||||
| [apiserver]: https://github.com/kubernetes/apiserver | ||||
| [core]: https://github.com/kubernetes/kubernetes | ||||
| [ingress]: https://github.com/kubernetes/ingress | ||||
| [kubeadm]: https://github.com/kubernetes/kubeadm | ||||
| [kubectl]: https://github.com/kubernetes/kubectl | ||||
| [instructions for moving]: ./HOWTOMOVE.md | ||||
|  | @ -0,0 +1,94 @@ | |||
| /* | ||||
| Copyright 2014 The Kubernetes Authors. | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
| 
 | ||||
|     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| 
 | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| package clock | ||||
| 
 | ||||
| import "time" | ||||
| 
 | ||||
| // Clock allows for injecting fake or real clocks into code that
 | ||||
| // needs to do arbitrary things based on time.
 | ||||
| type Clock interface { | ||||
| 	Now() time.Time | ||||
| 	Since(time.Time) time.Duration | ||||
| 	After(d time.Duration) <-chan time.Time | ||||
| 	NewTimer(d time.Duration) Timer | ||||
| 	Sleep(d time.Duration) | ||||
| 	Tick(d time.Duration) <-chan time.Time | ||||
| } | ||||
| 
 | ||||
| var _ = Clock(RealClock{}) | ||||
| 
 | ||||
| // RealClock really calls time.Now()
 | ||||
| type RealClock struct{} | ||||
| 
 | ||||
| // Now returns the current time.
 | ||||
| func (RealClock) Now() time.Time { | ||||
| 	return time.Now() | ||||
| } | ||||
| 
 | ||||
| // Since returns time since the specified timestamp.
 | ||||
| func (RealClock) Since(ts time.Time) time.Duration { | ||||
| 	return time.Since(ts) | ||||
| } | ||||
| 
 | ||||
| // Same as time.After(d).
 | ||||
| func (RealClock) After(d time.Duration) <-chan time.Time { | ||||
| 	return time.After(d) | ||||
| } | ||||
| 
 | ||||
| func (RealClock) NewTimer(d time.Duration) Timer { | ||||
| 	return &realTimer{ | ||||
| 		timer: time.NewTimer(d), | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (RealClock) Tick(d time.Duration) <-chan time.Time { | ||||
| 	return time.Tick(d) | ||||
| } | ||||
| 
 | ||||
| func (RealClock) Sleep(d time.Duration) { | ||||
| 	time.Sleep(d) | ||||
| } | ||||
| 
 | ||||
| // Timer allows for injecting fake or real timers into code that
 | ||||
| // needs to do arbitrary things based on time.
 | ||||
| type Timer interface { | ||||
| 	C() <-chan time.Time | ||||
| 	Stop() bool | ||||
| 	Reset(d time.Duration) bool | ||||
| } | ||||
| 
 | ||||
| var _ = Timer(&realTimer{}) | ||||
| 
 | ||||
| // realTimer is backed by an actual time.Timer.
 | ||||
| type realTimer struct { | ||||
| 	timer *time.Timer | ||||
| } | ||||
| 
 | ||||
| // C returns the underlying timer's channel.
 | ||||
| func (r *realTimer) C() <-chan time.Time { | ||||
| 	return r.timer.C | ||||
| } | ||||
| 
 | ||||
| // Stop calls Stop() on the underlying timer.
 | ||||
| func (r *realTimer) Stop() bool { | ||||
| 	return r.timer.Stop() | ||||
| } | ||||
| 
 | ||||
| // Reset calls Reset() on the underlying timer.
 | ||||
| func (r *realTimer) Reset(d time.Duration) bool { | ||||
| 	return r.timer.Reset(d) | ||||
| } | ||||
|  | @ -0,0 +1,254 @@ | |||
| /* | ||||
| Copyright 2014 The Kubernetes Authors. | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
| 
 | ||||
|     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| 
 | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| package testing | ||||
| 
 | ||||
| import ( | ||||
| 	"sync" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"k8s.io/utils/clock" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	_ = clock.Clock(&FakeClock{}) | ||||
| 	_ = clock.Clock(&IntervalClock{}) | ||||
| ) | ||||
| 
 | ||||
| // FakeClock implements clock.Clock, but returns an arbitrary time.
 | ||||
| type FakeClock struct { | ||||
| 	lock sync.RWMutex | ||||
| 	time time.Time | ||||
| 
 | ||||
| 	// waiters are waiting for the fake time to pass their specified time
 | ||||
| 	waiters []fakeClockWaiter | ||||
| } | ||||
| 
 | ||||
| type fakeClockWaiter struct { | ||||
| 	targetTime    time.Time | ||||
| 	stepInterval  time.Duration | ||||
| 	skipIfBlocked bool | ||||
| 	destChan      chan time.Time | ||||
| 	fired         bool | ||||
| } | ||||
| 
 | ||||
| func NewFakeClock(t time.Time) *FakeClock { | ||||
| 	return &FakeClock{ | ||||
| 		time: t, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // Now returns f's time.
 | ||||
| func (f *FakeClock) Now() time.Time { | ||||
| 	f.lock.RLock() | ||||
| 	defer f.lock.RUnlock() | ||||
| 	return f.time | ||||
| } | ||||
| 
 | ||||
| // Since returns time since the time in f.
 | ||||
| func (f *FakeClock) Since(ts time.Time) time.Duration { | ||||
| 	f.lock.RLock() | ||||
| 	defer f.lock.RUnlock() | ||||
| 	return f.time.Sub(ts) | ||||
| } | ||||
| 
 | ||||
| // Fake version of time.After(d).
 | ||||
| func (f *FakeClock) After(d time.Duration) <-chan time.Time { | ||||
| 	f.lock.Lock() | ||||
| 	defer f.lock.Unlock() | ||||
| 	stopTime := f.time.Add(d) | ||||
| 	ch := make(chan time.Time, 1) // Don't block!
 | ||||
| 	f.waiters = append(f.waiters, fakeClockWaiter{ | ||||
| 		targetTime: stopTime, | ||||
| 		destChan:   ch, | ||||
| 	}) | ||||
| 	return ch | ||||
| } | ||||
| 
 | ||||
| // Fake version of time.NewTimer(d).
 | ||||
| func (f *FakeClock) NewTimer(d time.Duration) clock.Timer { | ||||
| 	f.lock.Lock() | ||||
| 	defer f.lock.Unlock() | ||||
| 	stopTime := f.time.Add(d) | ||||
| 	ch := make(chan time.Time, 1) // Don't block!
 | ||||
| 	timer := &fakeTimer{ | ||||
| 		fakeClock: f, | ||||
| 		waiter: fakeClockWaiter{ | ||||
| 			targetTime: stopTime, | ||||
| 			destChan:   ch, | ||||
| 		}, | ||||
| 	} | ||||
| 	f.waiters = append(f.waiters, timer.waiter) | ||||
| 	return timer | ||||
| } | ||||
| 
 | ||||
| func (f *FakeClock) Tick(d time.Duration) <-chan time.Time { | ||||
| 	f.lock.Lock() | ||||
| 	defer f.lock.Unlock() | ||||
| 	tickTime := f.time.Add(d) | ||||
| 	ch := make(chan time.Time, 1) // hold one tick
 | ||||
| 	f.waiters = append(f.waiters, fakeClockWaiter{ | ||||
| 		targetTime:    tickTime, | ||||
| 		stepInterval:  d, | ||||
| 		skipIfBlocked: true, | ||||
| 		destChan:      ch, | ||||
| 	}) | ||||
| 
 | ||||
| 	return ch | ||||
| } | ||||
| 
 | ||||
| // Move clock by Duration, notify anyone that's called After, Tick, or NewTimer
 | ||||
| func (f *FakeClock) Step(d time.Duration) { | ||||
| 	f.lock.Lock() | ||||
| 	defer f.lock.Unlock() | ||||
| 	f.setTimeLocked(f.time.Add(d)) | ||||
| } | ||||
| 
 | ||||
| // Sets the time.
 | ||||
| func (f *FakeClock) SetTime(t time.Time) { | ||||
| 	f.lock.Lock() | ||||
| 	defer f.lock.Unlock() | ||||
| 	f.setTimeLocked(t) | ||||
| } | ||||
| 
 | ||||
| // Actually changes the time and checks any waiters. f must be write-locked.
 | ||||
| func (f *FakeClock) setTimeLocked(t time.Time) { | ||||
| 	f.time = t | ||||
| 	newWaiters := make([]fakeClockWaiter, 0, len(f.waiters)) | ||||
| 	for i := range f.waiters { | ||||
| 		w := &f.waiters[i] | ||||
| 		if !w.targetTime.After(t) { | ||||
| 
 | ||||
| 			if w.skipIfBlocked { | ||||
| 				select { | ||||
| 				case w.destChan <- t: | ||||
| 					w.fired = true | ||||
| 				default: | ||||
| 				} | ||||
| 			} else { | ||||
| 				w.destChan <- t | ||||
| 				w.fired = true | ||||
| 			} | ||||
| 
 | ||||
| 			if w.stepInterval > 0 { | ||||
| 				for !w.targetTime.After(t) { | ||||
| 					w.targetTime = w.targetTime.Add(w.stepInterval) | ||||
| 				} | ||||
| 				newWaiters = append(newWaiters, *w) | ||||
| 			} | ||||
| 
 | ||||
| 		} else { | ||||
| 			newWaiters = append(newWaiters, f.waiters[i]) | ||||
| 		} | ||||
| 	} | ||||
| 	f.waiters = newWaiters | ||||
| } | ||||
| 
 | ||||
| // Returns true if After has been called on f but not yet satisfied (so you can
 | ||||
| // write race-free tests).
 | ||||
| func (f *FakeClock) HasWaiters() bool { | ||||
| 	f.lock.RLock() | ||||
| 	defer f.lock.RUnlock() | ||||
| 	return len(f.waiters) > 0 | ||||
| } | ||||
| 
 | ||||
| func (f *FakeClock) Sleep(d time.Duration) { | ||||
| 	f.Step(d) | ||||
| } | ||||
| 
 | ||||
| // IntervalClock implements clock.Clock, but each invocation of Now steps the clock forward the specified duration
 | ||||
| type IntervalClock struct { | ||||
| 	Time     time.Time | ||||
| 	Duration time.Duration | ||||
| } | ||||
| 
 | ||||
| // Now returns i's time.
 | ||||
| func (i *IntervalClock) Now() time.Time { | ||||
| 	i.Time = i.Time.Add(i.Duration) | ||||
| 	return i.Time | ||||
| } | ||||
| 
 | ||||
| // Since returns time since the time in i.
 | ||||
| func (i *IntervalClock) Since(ts time.Time) time.Duration { | ||||
| 	return i.Time.Sub(ts) | ||||
| } | ||||
| 
 | ||||
| // Unimplemented, will panic.
 | ||||
| // TODO: make interval clock use FakeClock so this can be implemented.
 | ||||
| func (*IntervalClock) After(d time.Duration) <-chan time.Time { | ||||
| 	panic("IntervalClock doesn't implement After") | ||||
| } | ||||
| 
 | ||||
| // Unimplemented, will panic.
 | ||||
| // TODO: make interval clock use FakeClock so this can be implemented.
 | ||||
| func (*IntervalClock) NewTimer(d time.Duration) clock.Timer { | ||||
| 	panic("IntervalClock doesn't implement NewTimer") | ||||
| } | ||||
| 
 | ||||
| // Unimplemented, will panic.
 | ||||
| // TODO: make interval clock use FakeClock so this can be implemented.
 | ||||
| func (*IntervalClock) Tick(d time.Duration) <-chan time.Time { | ||||
| 	panic("IntervalClock doesn't implement Tick") | ||||
| } | ||||
| 
 | ||||
| func (*IntervalClock) Sleep(d time.Duration) { | ||||
| 	panic("IntervalClock doesn't implement Sleep") | ||||
| } | ||||
| 
 | ||||
| var _ = clock.Timer(&fakeTimer{}) | ||||
| 
 | ||||
| // fakeTimer implements clock.Timer based on a FakeClock.
 | ||||
| type fakeTimer struct { | ||||
| 	fakeClock *FakeClock | ||||
| 	waiter    fakeClockWaiter | ||||
| } | ||||
| 
 | ||||
| // C returns the channel that notifies when this timer has fired.
 | ||||
| func (f *fakeTimer) C() <-chan time.Time { | ||||
| 	return f.waiter.destChan | ||||
| } | ||||
| 
 | ||||
| // Stop stops the timer and returns true if the timer has not yet fired, or false otherwise.
 | ||||
| func (f *fakeTimer) Stop() bool { | ||||
| 	f.fakeClock.lock.Lock() | ||||
| 	defer f.fakeClock.lock.Unlock() | ||||
| 
 | ||||
| 	newWaiters := make([]fakeClockWaiter, 0, len(f.fakeClock.waiters)) | ||||
| 	for i := range f.fakeClock.waiters { | ||||
| 		w := &f.fakeClock.waiters[i] | ||||
| 		if w != &f.waiter { | ||||
| 			newWaiters = append(newWaiters, *w) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	f.fakeClock.waiters = newWaiters | ||||
| 
 | ||||
| 	return !f.waiter.fired | ||||
| } | ||||
| 
 | ||||
| // Reset resets the timer to the fake clock's "now" + d. It returns true if the timer has not yet
 | ||||
| // fired, or false otherwise.
 | ||||
| func (f *fakeTimer) Reset(d time.Duration) bool { | ||||
| 	f.fakeClock.lock.Lock() | ||||
| 	defer f.fakeClock.lock.Unlock() | ||||
| 
 | ||||
| 	active := !f.waiter.fired | ||||
| 
 | ||||
| 	f.waiter.fired = false | ||||
| 	f.waiter.targetTime = f.fakeClock.time.Add(d) | ||||
| 
 | ||||
| 	return active | ||||
| } | ||||
|  | @ -0,0 +1,184 @@ | |||
| /* | ||||
| Copyright 2015 The Kubernetes Authors. | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
| 
 | ||||
|     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| 
 | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| package testing | ||||
| 
 | ||||
| import ( | ||||
| 	"testing" | ||||
| 	"time" | ||||
| ) | ||||
| 
 | ||||
| func TestFakeClock(t *testing.T) { | ||||
| 	startTime := time.Now() | ||||
| 	tc := NewFakeClock(startTime) | ||||
| 	tc.Step(time.Second) | ||||
| 	now := tc.Now() | ||||
| 	if now.Sub(startTime) != time.Second { | ||||
| 		t.Errorf("input: %s now=%s gap=%s expected=%s", startTime, now, now.Sub(startTime), time.Second) | ||||
| 	} | ||||
| 
 | ||||
| 	tt := tc.Now() | ||||
| 	tc.SetTime(tt.Add(time.Hour)) | ||||
| 	if tc.Now().Sub(tt) != time.Hour { | ||||
| 		t.Errorf("input: %s now=%s gap=%s expected=%s", tt, tc.Now(), tc.Now().Sub(tt), time.Hour) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestFakeClockSleep(t *testing.T) { | ||||
| 	startTime := time.Now() | ||||
| 	tc := NewFakeClock(startTime) | ||||
| 	tc.Sleep(time.Duration(1) * time.Hour) | ||||
| 	now := tc.Now() | ||||
| 	if now.Sub(startTime) != time.Hour { | ||||
| 		t.Errorf("Fake sleep failed, expected time to advance by one hour, instead, its %v", now.Sub(startTime)) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestFakeAfter(t *testing.T) { | ||||
| 	tc := NewFakeClock(time.Now()) | ||||
| 	if tc.HasWaiters() { | ||||
| 		t.Errorf("unexpected waiter?") | ||||
| 	} | ||||
| 	oneSec := tc.After(time.Second) | ||||
| 	if !tc.HasWaiters() { | ||||
| 		t.Errorf("unexpected lack of waiter?") | ||||
| 	} | ||||
| 
 | ||||
| 	oneOhOneSec := tc.After(time.Second + time.Millisecond) | ||||
| 	twoSec := tc.After(2 * time.Second) | ||||
| 	select { | ||||
| 	case <-oneSec: | ||||
| 		t.Errorf("unexpected channel read") | ||||
| 	case <-oneOhOneSec: | ||||
| 		t.Errorf("unexpected channel read") | ||||
| 	case <-twoSec: | ||||
| 		t.Errorf("unexpected channel read") | ||||
| 	default: | ||||
| 	} | ||||
| 
 | ||||
| 	tc.Step(999 * time.Millisecond) | ||||
| 	select { | ||||
| 	case <-oneSec: | ||||
| 		t.Errorf("unexpected channel read") | ||||
| 	case <-oneOhOneSec: | ||||
| 		t.Errorf("unexpected channel read") | ||||
| 	case <-twoSec: | ||||
| 		t.Errorf("unexpected channel read") | ||||
| 	default: | ||||
| 	} | ||||
| 
 | ||||
| 	tc.Step(time.Millisecond) | ||||
| 	select { | ||||
| 	case <-oneSec: | ||||
| 		// Expected!
 | ||||
| 	case <-oneOhOneSec: | ||||
| 		t.Errorf("unexpected channel read") | ||||
| 	case <-twoSec: | ||||
| 		t.Errorf("unexpected channel read") | ||||
| 	default: | ||||
| 		t.Errorf("unexpected non-channel read") | ||||
| 	} | ||||
| 	tc.Step(time.Millisecond) | ||||
| 	select { | ||||
| 	case <-oneSec: | ||||
| 		// should not double-trigger!
 | ||||
| 		t.Errorf("unexpected channel read") | ||||
| 	case <-oneOhOneSec: | ||||
| 		// Expected!
 | ||||
| 	case <-twoSec: | ||||
| 		t.Errorf("unexpected channel read") | ||||
| 	default: | ||||
| 		t.Errorf("unexpected non-channel read") | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestFakeTick(t *testing.T) { | ||||
| 	tc := NewFakeClock(time.Now()) | ||||
| 	if tc.HasWaiters() { | ||||
| 		t.Errorf("unexpected waiter?") | ||||
| 	} | ||||
| 	oneSec := tc.Tick(time.Second) | ||||
| 	if !tc.HasWaiters() { | ||||
| 		t.Errorf("unexpected lack of waiter?") | ||||
| 	} | ||||
| 
 | ||||
| 	oneOhOneSec := tc.Tick(time.Second + time.Millisecond) | ||||
| 	twoSec := tc.Tick(2 * time.Second) | ||||
| 	select { | ||||
| 	case <-oneSec: | ||||
| 		t.Errorf("unexpected channel read") | ||||
| 	case <-oneOhOneSec: | ||||
| 		t.Errorf("unexpected channel read") | ||||
| 	case <-twoSec: | ||||
| 		t.Errorf("unexpected channel read") | ||||
| 	default: | ||||
| 	} | ||||
| 
 | ||||
| 	tc.Step(999 * time.Millisecond) // t=.999
 | ||||
| 	select { | ||||
| 	case <-oneSec: | ||||
| 		t.Errorf("unexpected channel read") | ||||
| 	case <-oneOhOneSec: | ||||
| 		t.Errorf("unexpected channel read") | ||||
| 	case <-twoSec: | ||||
| 		t.Errorf("unexpected channel read") | ||||
| 	default: | ||||
| 	} | ||||
| 
 | ||||
| 	tc.Step(time.Millisecond) // t=1.000
 | ||||
| 	select { | ||||
| 	case <-oneSec: | ||||
| 		// Expected!
 | ||||
| 	case <-oneOhOneSec: | ||||
| 		t.Errorf("unexpected channel read") | ||||
| 	case <-twoSec: | ||||
| 		t.Errorf("unexpected channel read") | ||||
| 	default: | ||||
| 		t.Errorf("unexpected non-channel read") | ||||
| 	} | ||||
| 	tc.Step(time.Millisecond) // t=1.001
 | ||||
| 	select { | ||||
| 	case <-oneSec: | ||||
| 		// should not double-trigger!
 | ||||
| 		t.Errorf("unexpected channel read") | ||||
| 	case <-oneOhOneSec: | ||||
| 		// Expected!
 | ||||
| 	case <-twoSec: | ||||
| 		t.Errorf("unexpected channel read") | ||||
| 	default: | ||||
| 		t.Errorf("unexpected non-channel read") | ||||
| 	} | ||||
| 
 | ||||
| 	tc.Step(time.Second) // t=2.001
 | ||||
| 	tc.Step(time.Second) // t=3.001
 | ||||
| 	tc.Step(time.Second) // t=4.001
 | ||||
| 	tc.Step(time.Second) // t=5.001
 | ||||
| 
 | ||||
| 	// The one second ticker should not accumulate ticks
 | ||||
| 	accumulatedTicks := 0 | ||||
| 	drained := false | ||||
| 	for !drained { | ||||
| 		select { | ||||
| 		case <-oneSec: | ||||
| 			accumulatedTicks++ | ||||
| 		default: | ||||
| 			drained = true | ||||
| 		} | ||||
| 	} | ||||
| 	if accumulatedTicks != 1 { | ||||
| 		t.Errorf("unexpected number of accumulated ticks: %d", accumulatedTicks) | ||||
| 	} | ||||
| } | ||||
|  | @ -0,0 +1,3 @@ | |||
| # Kubernetes Community Code of Conduct | ||||
| 
 | ||||
| Please refer to our [Kubernetes Community Code of Conduct](https://git.k8s.io/community/code-of-conduct.md) | ||||
|  | @ -0,0 +1,18 @@ | |||
| /* | ||||
| Copyright 2017 The Kubernetes Authors. | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
| 
 | ||||
|     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| 
 | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| // Package exec provides an injectable interface and implementations for running commands.
 | ||||
| package exec // import "k8s.io/utils/exec"
 | ||||
|  | @ -0,0 +1,215 @@ | |||
| /* | ||||
| Copyright 2017 The Kubernetes Authors. | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
| 
 | ||||
|     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| 
 | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| package exec | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"io" | ||||
| 	osexec "os/exec" | ||||
| 	"syscall" | ||||
| 	"time" | ||||
| ) | ||||
| 
 | ||||
| // ErrExecutableNotFound is returned if the executable is not found.
 | ||||
| var ErrExecutableNotFound = osexec.ErrNotFound | ||||
| 
 | ||||
| // Interface is an interface that presents a subset of the os/exec API. Use this
 | ||||
| // when you want to inject fakeable/mockable exec behavior.
 | ||||
| type Interface interface { | ||||
| 	// Command returns a Cmd instance which can be used to run a single command.
 | ||||
| 	// This follows the pattern of package os/exec.
 | ||||
| 	Command(cmd string, args ...string) Cmd | ||||
| 
 | ||||
| 	// CommandContext returns a Cmd instance which can be used to run a single command.
 | ||||
| 	//
 | ||||
| 	// The provided context is used to kill the process if the context becomes done
 | ||||
| 	// before the command completes on its own. For example, a timeout can be set in
 | ||||
| 	// the context.
 | ||||
| 	CommandContext(ctx context.Context, cmd string, args ...string) Cmd | ||||
| 
 | ||||
| 	// LookPath wraps os/exec.LookPath
 | ||||
| 	LookPath(file string) (string, error) | ||||
| } | ||||
| 
 | ||||
| // Cmd is an interface that presents an API that is very similar to Cmd from os/exec.
 | ||||
| // As more functionality is needed, this can grow. Since Cmd is a struct, we will have
 | ||||
| // to replace fields with get/set method pairs.
 | ||||
| type Cmd interface { | ||||
| 	// Run runs the command to the completion.
 | ||||
| 	Run() error | ||||
| 	// CombinedOutput runs the command and returns its combined standard output
 | ||||
| 	// and standard error. This follows the pattern of package os/exec.
 | ||||
| 	CombinedOutput() ([]byte, error) | ||||
| 	// Output runs the command and returns standard output, but not standard err
 | ||||
| 	Output() ([]byte, error) | ||||
| 	SetDir(dir string) | ||||
| 	SetStdin(in io.Reader) | ||||
| 	SetStdout(out io.Writer) | ||||
| 	SetStderr(out io.Writer) | ||||
| 	// Stops the command by sending SIGTERM. It is not guaranteed the
 | ||||
| 	// process will stop before this function returns. If the process is not
 | ||||
| 	// responding, an internal timer function will send a SIGKILL to force
 | ||||
| 	// terminate after 10 seconds.
 | ||||
| 	Stop() | ||||
| } | ||||
| 
 | ||||
| // ExitError is an interface that presents an API similar to os.ProcessState, which is
 | ||||
| // what ExitError from os/exec is. This is designed to make testing a bit easier and
 | ||||
| // probably loses some of the cross-platform properties of the underlying library.
 | ||||
| type ExitError interface { | ||||
| 	String() string | ||||
| 	Error() string | ||||
| 	Exited() bool | ||||
| 	ExitStatus() int | ||||
| } | ||||
| 
 | ||||
| // Implements Interface in terms of really exec()ing.
 | ||||
| type executor struct{} | ||||
| 
 | ||||
| // New returns a new Interface which will os/exec to run commands.
 | ||||
| func New() Interface { | ||||
| 	return &executor{} | ||||
| } | ||||
| 
 | ||||
| // Command is part of the Interface interface.
 | ||||
| func (executor *executor) Command(cmd string, args ...string) Cmd { | ||||
| 	return (*cmdWrapper)(osexec.Command(cmd, args...)) | ||||
| } | ||||
| 
 | ||||
| // CommandContext is part of the Interface interface.
 | ||||
| func (executor *executor) CommandContext(ctx context.Context, cmd string, args ...string) Cmd { | ||||
| 	return (*cmdWrapper)(osexec.CommandContext(ctx, cmd, args...)) | ||||
| } | ||||
| 
 | ||||
| // LookPath is part of the Interface interface
 | ||||
| func (executor *executor) LookPath(file string) (string, error) { | ||||
| 	return osexec.LookPath(file) | ||||
| } | ||||
| 
 | ||||
| // Wraps exec.Cmd so we can capture errors.
 | ||||
| type cmdWrapper osexec.Cmd | ||||
| 
 | ||||
| var _ Cmd = &cmdWrapper{} | ||||
| 
 | ||||
| func (cmd *cmdWrapper) SetDir(dir string) { | ||||
| 	cmd.Dir = dir | ||||
| } | ||||
| 
 | ||||
| func (cmd *cmdWrapper) SetStdin(in io.Reader) { | ||||
| 	cmd.Stdin = in | ||||
| } | ||||
| 
 | ||||
| func (cmd *cmdWrapper) SetStdout(out io.Writer) { | ||||
| 	cmd.Stdout = out | ||||
| } | ||||
| 
 | ||||
| func (cmd *cmdWrapper) SetStderr(out io.Writer) { | ||||
| 	cmd.Stderr = out | ||||
| } | ||||
| 
 | ||||
| // Run is part of the Cmd interface.
 | ||||
| func (cmd *cmdWrapper) Run() error { | ||||
| 	err := (*osexec.Cmd)(cmd).Run() | ||||
| 	return handleError(err) | ||||
| } | ||||
| 
 | ||||
| // CombinedOutput is part of the Cmd interface.
 | ||||
| func (cmd *cmdWrapper) CombinedOutput() ([]byte, error) { | ||||
| 	out, err := (*osexec.Cmd)(cmd).CombinedOutput() | ||||
| 	return out, handleError(err) | ||||
| } | ||||
| 
 | ||||
| func (cmd *cmdWrapper) Output() ([]byte, error) { | ||||
| 	out, err := (*osexec.Cmd)(cmd).Output() | ||||
| 	return out, handleError(err) | ||||
| } | ||||
| 
 | ||||
| // Stop is part of the Cmd interface.
 | ||||
| func (cmd *cmdWrapper) Stop() { | ||||
| 	c := (*osexec.Cmd)(cmd) | ||||
| 
 | ||||
| 	if c.Process == nil { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	c.Process.Signal(syscall.SIGTERM) | ||||
| 
 | ||||
| 	time.AfterFunc(10*time.Second, func() { | ||||
| 		if !c.ProcessState.Exited() { | ||||
| 			c.Process.Signal(syscall.SIGKILL) | ||||
| 		} | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| func handleError(err error) error { | ||||
| 	if err == nil { | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	switch e := err.(type) { | ||||
| 	case *osexec.ExitError: | ||||
| 		return &ExitErrorWrapper{e} | ||||
| 	case *osexec.Error: | ||||
| 		if e.Err == osexec.ErrNotFound { | ||||
| 			return ErrExecutableNotFound | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return err | ||||
| } | ||||
| 
 | ||||
| // ExitErrorWrapper is an implementation of ExitError in terms of os/exec ExitError.
 | ||||
| // Note: standard exec.ExitError is type *os.ProcessState, which already implements Exited().
 | ||||
| type ExitErrorWrapper struct { | ||||
| 	*osexec.ExitError | ||||
| } | ||||
| 
 | ||||
| var _ ExitError = &ExitErrorWrapper{} | ||||
| 
 | ||||
| // ExitStatus is part of the ExitError interface.
 | ||||
| func (eew ExitErrorWrapper) ExitStatus() int { | ||||
| 	ws, ok := eew.Sys().(syscall.WaitStatus) | ||||
| 	if !ok { | ||||
| 		panic("can't call ExitStatus() on a non-WaitStatus exitErrorWrapper") | ||||
| 	} | ||||
| 	return ws.ExitStatus() | ||||
| } | ||||
| 
 | ||||
| // CodeExitError is an implementation of ExitError consisting of an error object
 | ||||
| // and an exit code (the upper bits of os.exec.ExitStatus).
 | ||||
| type CodeExitError struct { | ||||
| 	Err  error | ||||
| 	Code int | ||||
| } | ||||
| 
 | ||||
| var _ ExitError = CodeExitError{} | ||||
| 
 | ||||
| func (e CodeExitError) Error() string { | ||||
| 	return e.Err.Error() | ||||
| } | ||||
| 
 | ||||
| func (e CodeExitError) String() string { | ||||
| 	return e.Err.Error() | ||||
| } | ||||
| 
 | ||||
| func (e CodeExitError) Exited() bool { | ||||
| 	return true | ||||
| } | ||||
| 
 | ||||
| func (e CodeExitError) ExitStatus() int { | ||||
| 	return e.Code | ||||
| } | ||||
|  | @ -0,0 +1,141 @@ | |||
| /* | ||||
| Copyright 2017 The Kubernetes Authors. | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
| 
 | ||||
|     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| 
 | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| package exec | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	osexec "os/exec" | ||||
| 	"testing" | ||||
| 	"time" | ||||
| ) | ||||
| 
 | ||||
| func TestExecutorNoArgs(t *testing.T) { | ||||
| 	ex := New() | ||||
| 
 | ||||
| 	cmd := ex.Command("true") | ||||
| 	out, err := cmd.CombinedOutput() | ||||
| 	if err != nil { | ||||
| 		t.Errorf("expected success, got %v", err) | ||||
| 	} | ||||
| 	if len(out) != 0 { | ||||
| 		t.Errorf("expected no output, got %q", string(out)) | ||||
| 	} | ||||
| 
 | ||||
| 	cmd = ex.Command("false") | ||||
| 	out, err = cmd.CombinedOutput() | ||||
| 	if err == nil { | ||||
| 		t.Errorf("expected failure, got nil error") | ||||
| 	} | ||||
| 	if len(out) != 0 { | ||||
| 		t.Errorf("expected no output, got %q", string(out)) | ||||
| 	} | ||||
| 	ee, ok := err.(ExitError) | ||||
| 	if !ok { | ||||
| 		t.Errorf("expected an ExitError, got %+v", err) | ||||
| 	} | ||||
| 	if ee.Exited() { | ||||
| 		if code := ee.ExitStatus(); code != 1 { | ||||
| 			t.Errorf("expected exit status 1, got %d", code) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	cmd = ex.Command("/does/not/exist") | ||||
| 	out, err = cmd.CombinedOutput() | ||||
| 	if err == nil { | ||||
| 		t.Errorf("expected failure, got nil error") | ||||
| 	} | ||||
| 	if ee, ok := err.(ExitError); ok { | ||||
| 		t.Errorf("expected non-ExitError, got %+v", ee) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestExecutorWithArgs(t *testing.T) { | ||||
| 	ex := New() | ||||
| 
 | ||||
| 	cmd := ex.Command("echo", "stdout") | ||||
| 	out, err := cmd.CombinedOutput() | ||||
| 	if err != nil { | ||||
| 		t.Errorf("expected success, got %+v", err) | ||||
| 	} | ||||
| 	if string(out) != "stdout\n" { | ||||
| 		t.Errorf("unexpected output: %q", string(out)) | ||||
| 	} | ||||
| 
 | ||||
| 	cmd = ex.Command("/bin/sh", "-c", "echo stderr > /dev/stderr") | ||||
| 	out, err = cmd.CombinedOutput() | ||||
| 	if err != nil { | ||||
| 		t.Errorf("expected success, got %+v", err) | ||||
| 	} | ||||
| 	if string(out) != "stderr\n" { | ||||
| 		t.Errorf("unexpected output: %q", string(out)) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestLookPath(t *testing.T) { | ||||
| 	ex := New() | ||||
| 
 | ||||
| 	shExpected, _ := osexec.LookPath("sh") | ||||
| 	sh, _ := ex.LookPath("sh") | ||||
| 	if sh != shExpected { | ||||
| 		t.Errorf("unexpected result for LookPath: got %s, expected %s", sh, shExpected) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestExecutableNotFound(t *testing.T) { | ||||
| 	exec := New() | ||||
| 
 | ||||
| 	cmd := exec.Command("fake_executable_name") | ||||
| 	_, err := cmd.CombinedOutput() | ||||
| 	if err != ErrExecutableNotFound { | ||||
| 		t.Errorf("cmd.CombinedOutput(): Expected error ErrExecutableNotFound but got %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	cmd = exec.Command("fake_executable_name") | ||||
| 	_, err = cmd.Output() | ||||
| 	if err != ErrExecutableNotFound { | ||||
| 		t.Errorf("cmd.Output(): Expected error ErrExecutableNotFound but got %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	cmd = exec.Command("fake_executable_name") | ||||
| 	err = cmd.Run() | ||||
| 	if err != ErrExecutableNotFound { | ||||
| 		t.Errorf("cmd.Run(): Expected error ErrExecutableNotFound but got %v", err) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestStopBeforeStart(t *testing.T) { | ||||
| 	cmd := New().Command("echo", "hello") | ||||
| 
 | ||||
| 	// no panic calling Stop before calling Run
 | ||||
| 	cmd.Stop() | ||||
| 
 | ||||
| 	cmd.Run() | ||||
| 
 | ||||
| 	// no panic calling Stop after command is done
 | ||||
| 	cmd.Stop() | ||||
| } | ||||
| 
 | ||||
| func TestTimeout(t *testing.T) { | ||||
| 	exec := New() | ||||
| 	ctx, cancel := context.WithTimeout(context.Background(), time.Nanosecond) | ||||
| 	defer cancel() | ||||
| 
 | ||||
| 	err := exec.CommandContext(ctx, "sleep", "2").Run() | ||||
| 	if err != context.DeadlineExceeded { | ||||
| 		t.Errorf("expected %v but got %v", context.DeadlineExceeded, err) | ||||
| 	} | ||||
| } | ||||
|  | @ -0,0 +1,37 @@ | |||
| /* | ||||
| Copyright 2017 The Kubernetes Authors. | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
| 
 | ||||
|     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| 
 | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| package exec_test | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"fmt" | ||||
| 
 | ||||
| 	"k8s.io/utils/exec" | ||||
| ) | ||||
| 
 | ||||
| func ExampleNew() { | ||||
| 	exec := exec.New() | ||||
| 
 | ||||
| 	cmd := exec.Command("echo", "Bonjour!") | ||||
| 	buff := bytes.Buffer{} | ||||
| 	cmd.SetStdout(&buff) | ||||
| 	if err := cmd.Run(); err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 	fmt.Println(buff.String()) | ||||
| 	// Output: Bonjour!
 | ||||
| } | ||||
|  | @ -0,0 +1,158 @@ | |||
| /* | ||||
| Copyright 2017 The Kubernetes Authors. | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
| 
 | ||||
|     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| 
 | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| package testingexec | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 
 | ||||
| 	"k8s.io/utils/exec" | ||||
| ) | ||||
| 
 | ||||
| // A simple scripted Interface type.
 | ||||
| type FakeExec struct { | ||||
| 	CommandScript []FakeCommandAction | ||||
| 	CommandCalls  int | ||||
| 	LookPathFunc  func(string) (string, error) | ||||
| } | ||||
| 
 | ||||
| var _ exec.Interface = &FakeExec{} | ||||
| 
 | ||||
| type FakeCommandAction func(cmd string, args ...string) exec.Cmd | ||||
| 
 | ||||
| func (fake *FakeExec) Command(cmd string, args ...string) exec.Cmd { | ||||
| 	if fake.CommandCalls > len(fake.CommandScript)-1 { | ||||
| 		panic(fmt.Sprintf("ran out of Command() actions. Could not handle command [%d]: %s args: %v", fake.CommandCalls, cmd, args)) | ||||
| 	} | ||||
| 	i := fake.CommandCalls | ||||
| 	fake.CommandCalls++ | ||||
| 	return fake.CommandScript[i](cmd, args...) | ||||
| } | ||||
| 
 | ||||
| func (fake *FakeExec) CommandContext(ctx context.Context, cmd string, args ...string) exec.Cmd { | ||||
| 	return fake.Command(cmd, args...) | ||||
| } | ||||
| 
 | ||||
| func (fake *FakeExec) LookPath(file string) (string, error) { | ||||
| 	return fake.LookPathFunc(file) | ||||
| } | ||||
| 
 | ||||
| // A simple scripted Cmd type.
 | ||||
| type FakeCmd struct { | ||||
| 	Argv                 []string | ||||
| 	CombinedOutputScript []FakeCombinedOutputAction | ||||
| 	CombinedOutputCalls  int | ||||
| 	CombinedOutputLog    [][]string | ||||
| 	RunScript            []FakeRunAction | ||||
| 	RunCalls             int | ||||
| 	RunLog               [][]string | ||||
| 	Dirs                 []string | ||||
| 	Stdin                io.Reader | ||||
| 	Stdout               io.Writer | ||||
| 	Stderr               io.Writer | ||||
| } | ||||
| 
 | ||||
| var _ exec.Cmd = &FakeCmd{} | ||||
| 
 | ||||
| func InitFakeCmd(fake *FakeCmd, cmd string, args ...string) exec.Cmd { | ||||
| 	fake.Argv = append([]string{cmd}, args...) | ||||
| 	return fake | ||||
| } | ||||
| 
 | ||||
| type FakeCombinedOutputAction func() ([]byte, error) | ||||
| type FakeRunAction func() ([]byte, []byte, error) | ||||
| 
 | ||||
| func (fake *FakeCmd) SetDir(dir string) { | ||||
| 	fake.Dirs = append(fake.Dirs, dir) | ||||
| } | ||||
| 
 | ||||
| func (fake *FakeCmd) SetStdin(in io.Reader) { | ||||
| 	fake.Stdin = in | ||||
| } | ||||
| 
 | ||||
| func (fake *FakeCmd) SetStdout(out io.Writer) { | ||||
| 	fake.Stdout = out | ||||
| } | ||||
| 
 | ||||
| func (fake *FakeCmd) SetStderr(out io.Writer) { | ||||
| 	fake.Stderr = out | ||||
| } | ||||
| 
 | ||||
| func (fake *FakeCmd) Run() error { | ||||
| 	if fake.RunCalls > len(fake.RunScript)-1 { | ||||
| 		panic("ran out of Run() actions") | ||||
| 	} | ||||
| 	if fake.RunLog == nil { | ||||
| 		fake.RunLog = [][]string{} | ||||
| 	} | ||||
| 	i := fake.RunCalls | ||||
| 	fake.RunLog = append(fake.RunLog, append([]string{}, fake.Argv...)) | ||||
| 	fake.RunCalls++ | ||||
| 	stdout, stderr, err := fake.RunScript[i]() | ||||
| 	if stdout != nil { | ||||
| 		fake.Stdout.Write(stdout) | ||||
| 	} | ||||
| 	if stderr != nil { | ||||
| 		fake.Stderr.Write(stderr) | ||||
| 	} | ||||
| 	return err | ||||
| } | ||||
| 
 | ||||
| func (fake *FakeCmd) CombinedOutput() ([]byte, error) { | ||||
| 	if fake.CombinedOutputCalls > len(fake.CombinedOutputScript)-1 { | ||||
| 		panic("ran out of CombinedOutput() actions") | ||||
| 	} | ||||
| 	if fake.CombinedOutputLog == nil { | ||||
| 		fake.CombinedOutputLog = [][]string{} | ||||
| 	} | ||||
| 	i := fake.CombinedOutputCalls | ||||
| 	fake.CombinedOutputLog = append(fake.CombinedOutputLog, append([]string{}, fake.Argv...)) | ||||
| 	fake.CombinedOutputCalls++ | ||||
| 	return fake.CombinedOutputScript[i]() | ||||
| } | ||||
| 
 | ||||
| func (fake *FakeCmd) Output() ([]byte, error) { | ||||
| 	return nil, fmt.Errorf("unimplemented") | ||||
| } | ||||
| 
 | ||||
| func (fake *FakeCmd) Stop() { | ||||
| 	// no-op
 | ||||
| } | ||||
| 
 | ||||
| // A simple fake ExitError type.
 | ||||
| type FakeExitError struct { | ||||
| 	Status int | ||||
| } | ||||
| 
 | ||||
| var _ exec.ExitError = FakeExitError{} | ||||
| 
 | ||||
| func (fake FakeExitError) String() string { | ||||
| 	return fmt.Sprintf("exit %d", fake.Status) | ||||
| } | ||||
| 
 | ||||
| func (fake FakeExitError) Error() string { | ||||
| 	return fake.String() | ||||
| } | ||||
| 
 | ||||
| func (fake FakeExitError) Exited() bool { | ||||
| 	return true | ||||
| } | ||||
| 
 | ||||
| func (fake FakeExitError) ExitStatus() int { | ||||
| 	return fake.Status | ||||
| } | ||||
|  | @ -0,0 +1,70 @@ | |||
| /* | ||||
| Copyright 2017 The Kubernetes Authors. | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
| 
 | ||||
|     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| 
 | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| package temp | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"io/ioutil" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| ) | ||||
| 
 | ||||
| // Directory is an interface to a temporary directory, in which you can
 | ||||
| // create new files.
 | ||||
| type Directory interface { | ||||
| 	// NewFile creates a new file in that directory. Calling NewFile
 | ||||
| 	// with the same filename twice will result in an error.
 | ||||
| 	NewFile(name string) (io.WriteCloser, error) | ||||
| 	// Delete removes the directory and its content.
 | ||||
| 	Delete() error | ||||
| } | ||||
| 
 | ||||
| // TempDir is wrapping an temporary directory on disk.
 | ||||
| type TempDir struct { | ||||
| 	// Name is the name (full path) of the created directory.
 | ||||
| 	Name string | ||||
| } | ||||
| 
 | ||||
| var _ Directory = &TempDir{} | ||||
| 
 | ||||
| // CreateTempDir returns a new Directory wrapping a temporary directory
 | ||||
| // on disk.
 | ||||
| func CreateTempDir(prefix string) (*TempDir, error) { | ||||
| 	name, err := ioutil.TempDir("", fmt.Sprintf("%s-", prefix)) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return &TempDir{ | ||||
| 		Name: name, | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| // NewFile creates a new file in the specified directory.
 | ||||
| func (d *TempDir) NewFile(name string) (io.WriteCloser, error) { | ||||
| 	return os.OpenFile( | ||||
| 		filepath.Join(d.Name, name), | ||||
| 		os.O_WRONLY|os.O_CREATE|os.O_TRUNC|os.O_EXCL, | ||||
| 		0700, | ||||
| 	) | ||||
| } | ||||
| 
 | ||||
| // Delete the underlying directory, and all of its content.
 | ||||
| func (d *TempDir) Delete() error { | ||||
| 	return os.RemoveAll(d.Name) | ||||
| } | ||||
|  | @ -0,0 +1,90 @@ | |||
| /* | ||||
| Copyright 2017 The Kubernetes Authors. | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
| 
 | ||||
|     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| 
 | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| package temp | ||||
| 
 | ||||
| import ( | ||||
| 	"io/ioutil" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
| ) | ||||
| 
 | ||||
| func TestTempDir(t *testing.T) { | ||||
| 	dir, err := CreateTempDir("prefix") | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	// Delete the directory no matter what.
 | ||||
| 	defer dir.Delete() | ||||
| 
 | ||||
| 	// Make sure we have created the dir, with the proper name
 | ||||
| 	_, err = os.Stat(dir.Name) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	if !strings.HasPrefix(filepath.Base(dir.Name), "prefix") { | ||||
| 		t.Fatalf(`Directory doesn't start with "prefix": %q`, | ||||
| 			dir.Name) | ||||
| 	} | ||||
| 
 | ||||
| 	// Verify that the directory is empty
 | ||||
| 	entries, err := ioutil.ReadDir(dir.Name) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	if len(entries) != 0 { | ||||
| 		t.Fatalf("Directory should be empty, has %d elements", | ||||
| 			len(entries)) | ||||
| 	} | ||||
| 
 | ||||
| 	// Create a couple of files
 | ||||
| 	_, err = dir.NewFile("ONE") | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	_, err = dir.NewFile("TWO") | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	// We can't create the same file twice
 | ||||
| 	_, err = dir.NewFile("TWO") | ||||
| 	if err == nil { | ||||
| 		t.Fatal("NewFile should fail to create the same file twice") | ||||
| 	} | ||||
| 
 | ||||
| 	// We have created only two files
 | ||||
| 	entries, err = ioutil.ReadDir(dir.Name) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	if len(entries) != 2 { | ||||
| 		t.Fatalf("ReadDir should have two elements, has %d elements", | ||||
| 			len(entries)) | ||||
| 	} | ||||
| 
 | ||||
| 	// Verify that deletion works
 | ||||
| 	err = dir.Delete() | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	_, err = os.Stat(dir.Name) | ||||
| 	if err == nil { | ||||
| 		t.Fatal("Directory should be gone, still present.") | ||||
| 	} | ||||
| } | ||||
|  | @ -0,0 +1,19 @@ | |||
| /* | ||||
| Copyright 2017 The Kubernetes Authors. | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
| 
 | ||||
|     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| 
 | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| // Package temp provides an interface to handle temporary files and
 | ||||
| // directories.
 | ||||
| package temp // import "k8s.io/utils/temp"
 | ||||
|  | @ -0,0 +1,66 @@ | |||
| /* | ||||
| Copyright 2017 The Kubernetes Authors. | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
| 
 | ||||
|     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| 
 | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| package temptest | ||||
| 
 | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 
 | ||||
| 	"k8s.io/utils/temp" | ||||
| ) | ||||
| 
 | ||||
| // FakeDir implements a Directory that is not backed on the
 | ||||
| // filesystem. This is useful for testing since the created "files" are
 | ||||
| // simple bytes.Buffer that can be inspected.
 | ||||
| type FakeDir struct { | ||||
| 	Files   map[string]*FakeFile | ||||
| 	Deleted bool | ||||
| } | ||||
| 
 | ||||
| var _ temp.Directory = &FakeDir{} | ||||
| 
 | ||||
| // NewFile returns a new FakeFile if the filename doesn't exist already.
 | ||||
| // This function will fail if the directory has already been deleted.
 | ||||
| func (d *FakeDir) NewFile(name string) (io.WriteCloser, error) { | ||||
| 	if d.Deleted { | ||||
| 		return nil, errors.New("can't create file in deleted FakeDir") | ||||
| 	} | ||||
| 	if d.Files == nil { | ||||
| 		d.Files = map[string]*FakeFile{} | ||||
| 	} | ||||
| 	f := d.Files[name] | ||||
| 	if f != nil { | ||||
| 		return nil, fmt.Errorf( | ||||
| 			"FakeDir already has file named %q", | ||||
| 			name, | ||||
| 		) | ||||
| 	} | ||||
| 	f = &FakeFile{} | ||||
| 	d.Files[name] = f | ||||
| 	return f, nil | ||||
| } | ||||
| 
 | ||||
| // Delete doesn't remove anything, but records that the directory has
 | ||||
| // been deleted.
 | ||||
| func (d *FakeDir) Delete() error { | ||||
| 	if d.Deleted { | ||||
| 		return errors.New("failed to re-delete FakeDir") | ||||
| 	} | ||||
| 	d.Deleted = true | ||||
| 	return nil | ||||
| } | ||||
|  | @ -0,0 +1,67 @@ | |||
| /* | ||||
| Copyright 2017 The Kubernetes Authors. | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
| 
 | ||||
|     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| 
 | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| package temptest | ||||
| 
 | ||||
| import ( | ||||
| 	"io" | ||||
| 	"testing" | ||||
| ) | ||||
| 
 | ||||
| func TestFakeDir(t *testing.T) { | ||||
| 	d := &FakeDir{} | ||||
| 
 | ||||
| 	f, err := d.NewFile("ONE") | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	n, err := io.WriteString(f, "Bonjour!") | ||||
| 	if n != 8 || err != nil { | ||||
| 		t.Fatalf( | ||||
| 			`WriteString(f, "Bonjour!") = (%v, %v), expected (%v, %v)`, | ||||
| 			n, err, | ||||
| 			0, nil, | ||||
| 		) | ||||
| 	} | ||||
| 	if got := d.Files["ONE"].Buffer.String(); got != "Bonjour!" { | ||||
| 		t.Fatalf(`file content is %q, expected "Bonjour!"`, got) | ||||
| 	} | ||||
| 
 | ||||
| 	f, err = d.NewFile("ONE") | ||||
| 	if err == nil { | ||||
| 		t.Fatal("Same file could be created twice.") | ||||
| 	} | ||||
| 
 | ||||
| 	err = d.Delete() | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	err = d.Delete() | ||||
| 	if err == nil { | ||||
| 		t.Fatal("FakeDir could be deleted twice.") | ||||
| 	} | ||||
| 
 | ||||
| 	f, err = d.NewFile("TWO") | ||||
| 	if err == nil { | ||||
| 		t.Fatal("NewFile could be created in deleted dir") | ||||
| 	} | ||||
| 
 | ||||
| 	if !d.Deleted { | ||||
| 		t.Fatal("FakeDir should be deleted.") | ||||
| 	} | ||||
| } | ||||
|  | @ -0,0 +1,19 @@ | |||
| /* | ||||
| Copyright 2017 The Kubernetes Authors. | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
| 
 | ||||
|     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| 
 | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| // Package temptest provides utilities for testing temp
 | ||||
| // files/directories testing.
 | ||||
| package temptest | ||||
|  | @ -0,0 +1,57 @@ | |||
| /* | ||||
| Copyright 2017 The Kubernetes Authors. | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
| 
 | ||||
|     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| 
 | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| package temptest | ||||
| 
 | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 
 | ||||
| 	"k8s.io/utils/temp" | ||||
| ) | ||||
| 
 | ||||
| func TestedCode(dir temp.Directory) error { | ||||
| 	f, err := dir.NewFile("filename") | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	_, err = io.WriteString(f, "Bonjour!") | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	return dir.Delete() | ||||
| } | ||||
| 
 | ||||
| func Example() { | ||||
| 	dir := FakeDir{} | ||||
| 
 | ||||
| 	err := TestedCode(&dir) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 
 | ||||
| 	if dir.Deleted == false { | ||||
| 		panic(errors.New("Directory should have been deleted.")) | ||||
| 	} | ||||
| 
 | ||||
| 	if dir.Files["filename"] == nil { | ||||
| 		panic(errors.New(`"filename" should have been created.`)) | ||||
| 	} | ||||
| 
 | ||||
| 	fmt.Println(dir.Files["filename"].Buffer.String()) | ||||
| 	// Output: Bonjour!
 | ||||
| } | ||||
|  | @ -0,0 +1,52 @@ | |||
| /* | ||||
| Copyright 2017 The Kubernetes Authors. | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
| 
 | ||||
|     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| 
 | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| package temptest | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"errors" | ||||
| 	"io" | ||||
| ) | ||||
| 
 | ||||
| // FakeFile is an implementation of a WriteCloser, that records what has
 | ||||
| // been written in the file (in a bytes.Buffer) and if the file has been
 | ||||
| // closed.
 | ||||
| type FakeFile struct { | ||||
| 	Buffer bytes.Buffer | ||||
| 	Closed bool | ||||
| } | ||||
| 
 | ||||
| var _ io.WriteCloser = &FakeFile{} | ||||
| 
 | ||||
| // Write appends the contents of p to the Buffer. If the file has
 | ||||
| // already been closed, an error is returned.
 | ||||
| func (f *FakeFile) Write(p []byte) (n int, err error) { | ||||
| 	if f.Closed { | ||||
| 		return 0, errors.New("can't write to closed FakeFile") | ||||
| 	} | ||||
| 	return f.Buffer.Write(p) | ||||
| } | ||||
| 
 | ||||
| // Close records that the file has been closed. If the file has already
 | ||||
| // been closed, an error is returned.
 | ||||
| func (f *FakeFile) Close() error { | ||||
| 	if f.Closed { | ||||
| 		return errors.New("FakeFile was closed multiple times") | ||||
| 	} | ||||
| 	f.Closed = true | ||||
| 	return nil | ||||
| } | ||||
|  | @ -0,0 +1,56 @@ | |||
| /* | ||||
| Copyright 2017 The Kubernetes Authors. | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
| 
 | ||||
|     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| 
 | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| package temptest | ||||
| 
 | ||||
| import ( | ||||
| 	"io" | ||||
| 	"testing" | ||||
| ) | ||||
| 
 | ||||
| func TestFakeFile(t *testing.T) { | ||||
| 	f := &FakeFile{} | ||||
| 
 | ||||
| 	n, err := io.WriteString(f, "Bonjour!") | ||||
| 	if n != 8 || err != nil { | ||||
| 		t.Fatalf( | ||||
| 			`WriteString(f, "Bonjour!") = (%v, %v), expected (%v, %v)`, | ||||
| 			n, err, | ||||
| 			8, nil, | ||||
| 		) | ||||
| 	} | ||||
| 
 | ||||
| 	err = f.Close() | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	// File can't be closed twice.
 | ||||
| 	err = f.Close() | ||||
| 	if err == nil { | ||||
| 		t.Fatal("FakeFile could be closed twice") | ||||
| 	} | ||||
| 
 | ||||
| 	// File is not writable after close.
 | ||||
| 	n, err = io.WriteString(f, "Bonjour!") | ||||
| 	if n != 0 || err == nil { | ||||
| 		t.Fatalf( | ||||
| 			`WriteString(f, "Bonjour!") = (%v, %v), expected (%v, %v)`, | ||||
| 			n, err, | ||||
| 			0, "non-nil", | ||||
| 		) | ||||
| 	} | ||||
| } | ||||
		Loading…
	
		Reference in New Issue