Skip to content

Commit b07f88c

Browse files
committed
Add a fake of the HostOrchestratorClient interface
Bug: b/436913689
1 parent 8ae6717 commit b07f88c

File tree

2 files changed

+314
-0
lines changed

2 files changed

+314
-0
lines changed
Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package libhoclient
16+
17+
import (
18+
"bytes"
19+
"encoding/json"
20+
"fmt"
21+
"io"
22+
"time"
23+
24+
hoapi "github.com/google/android-cuttlefish/frontend/src/host_orchestrator/api/v1"
25+
wclient "github.com/google/android-cuttlefish/frontend/src/libhoclient/webrtcclient"
26+
"github.com/gorilla/websocket"
27+
)
28+
29+
// Fake implementation of the HostOrchestratorClient interface for use in testing
30+
type FakeHostOrchestratorClient struct {
31+
cvds map[int]*hoapi.CVD
32+
operations map[string]chan []byte
33+
imageDirs []string
34+
}
35+
36+
func NewFakeHostOrchestratorClient() *FakeHostOrchestratorClient {
37+
return &FakeHostOrchestratorClient{
38+
cvds: make(map[int]*hoapi.CVD),
39+
operations: make(map[string]chan []byte),
40+
imageDirs: []string{},
41+
}
42+
}
43+
44+
func (c *FakeHostOrchestratorClient) ConnectWebRTC(device string, observer wclient.Observer, logger io.Writer, opts ConnectWebRTCOpts) (*wclient.Connection, error) {
45+
return nil, nil
46+
}
47+
48+
func (c *FakeHostOrchestratorClient) ConnectADBWebSocket(device string) (*websocket.Conn, error) {
49+
return nil, nil
50+
}
51+
52+
func (c *FakeHostOrchestratorClient) FetchArtifacts(req *hoapi.FetchArtifactsRequest, creds BuildAPICreds) (*hoapi.FetchArtifactsResponse, error) {
53+
return &hoapi.FetchArtifactsResponse{AndroidCIBundle: req.AndroidCIBundle}, nil
54+
}
55+
56+
func (c *FakeHostOrchestratorClient) CreateCVD(req *hoapi.CreateCVDRequest, creds BuildAPICreds) (*hoapi.CreateCVDResponse, error) {
57+
var cvds []*hoapi.CVD
58+
var err error
59+
if req.CVD != nil {
60+
cvds, err = c.createFakeCVDs(int(req.AdditionalInstancesNum) + 1)
61+
} else {
62+
cvds, err = c.createFakeEnvironment(req.EnvConfig)
63+
}
64+
return &hoapi.CreateCVDResponse{CVDs: cvds}, err
65+
}
66+
67+
func (c *FakeHostOrchestratorClient) CreateCVDOp(req *hoapi.CreateCVDRequest, creds BuildAPICreds) (*hoapi.Operation, error) {
68+
op, ch := c.newFakeOperation()
69+
go func() {
70+
res, err := c.CreateCVD(req, creds)
71+
var msg []byte
72+
if err != nil {
73+
msg, _ = json.Marshal(err)
74+
} else {
75+
msg, _ = json.Marshal(res)
76+
}
77+
ch <- msg
78+
}()
79+
return op, nil
80+
}
81+
82+
func (c *FakeHostOrchestratorClient) DeleteCVD(id string) error {
83+
for k, cvd := range c.cvds {
84+
if cvd.Group == id {
85+
delete(c.cvds, k)
86+
}
87+
}
88+
return nil
89+
}
90+
91+
func (c *FakeHostOrchestratorClient) ListCVDs() ([]*hoapi.CVD, error) {
92+
ret := []*hoapi.CVD{}
93+
for _, cvd := range c.cvds {
94+
ret = append(ret, cvd)
95+
}
96+
return ret, nil
97+
}
98+
99+
func (c *FakeHostOrchestratorClient) UploadArtifact(filename string) error {
100+
return nil
101+
}
102+
103+
func (c *FakeHostOrchestratorClient) ExtractArtifact(filename string) (*hoapi.Operation, error) {
104+
op, ch := c.newFakeOperation()
105+
go func() {
106+
ch <- []byte("done")
107+
}()
108+
return op, nil
109+
}
110+
111+
func (c *FakeHostOrchestratorClient) CreateImageDirectory() (*hoapi.Operation, error) {
112+
op, ch := c.newFakeOperation()
113+
go func() {
114+
dir := c.randomString()
115+
c.imageDirs = append(c.imageDirs, dir)
116+
msg, _ := json.Marshal(&hoapi.CreateImageDirectoryResponse{
117+
ID: dir,
118+
})
119+
ch <- msg
120+
}()
121+
return op, nil
122+
}
123+
124+
func (c *FakeHostOrchestratorClient) ListImageDirectories() (*hoapi.ListImageDirectoriesResponse, error) {
125+
imageDirs := []hoapi.ImageDirectory{}
126+
for _, dir := range c.imageDirs {
127+
imageDirs = append(imageDirs, hoapi.ImageDirectory{ID: dir})
128+
}
129+
return &hoapi.ListImageDirectoriesResponse{
130+
ImageDirs: imageDirs,
131+
}, nil
132+
}
133+
134+
func (c *FakeHostOrchestratorClient) UpdateImageDirectoryWithUserArtifact(id, filename string) (*hoapi.Operation, error) {
135+
op, ch := c.newFakeOperation()
136+
go func() {
137+
ch <- []byte("done")
138+
}()
139+
return op, nil
140+
}
141+
142+
func (c *FakeHostOrchestratorClient) DeleteImageDirectory(id string) (*hoapi.Operation, error) {
143+
op, ch := c.newFakeOperation()
144+
go func() {
145+
newImageDirs := []string{}
146+
for _, dir := range c.imageDirs {
147+
if dir != id {
148+
newImageDirs = append(newImageDirs, dir)
149+
}
150+
}
151+
c.imageDirs = newImageDirs
152+
ch <- []byte("done")
153+
}()
154+
return op, nil
155+
}
156+
157+
func (c *FakeHostOrchestratorClient) WaitForOperation(opName string, result any) error {
158+
ch, ok := c.operations[opName]
159+
if !ok {
160+
return fmt.Errorf("operation not found")
161+
}
162+
if ch == nil {
163+
return fmt.Errorf("operation already waited for")
164+
}
165+
res := <-ch
166+
decoder := json.NewDecoder(bytes.NewReader(res))
167+
return decoder.Decode(result)
168+
}
169+
170+
func (c *FakeHostOrchestratorClient) CreateBugReport(group string, opts CreateBugReportOpts, dst io.Writer) error {
171+
dst.Write([]byte("bugreport"))
172+
return nil
173+
}
174+
175+
func (c *FakeHostOrchestratorClient) Powerwash(groupName, instanceName string) error { return nil }
176+
177+
func (c *FakeHostOrchestratorClient) Stop(groupName, instanceName string) error { return nil }
178+
179+
func (c *FakeHostOrchestratorClient) Start(groupName, instanceName string, req *hoapi.StartCVDRequest) error {
180+
return nil
181+
}
182+
183+
func (c *FakeHostOrchestratorClient) CreateSnapshot(groupName, instanceName string, req *hoapi.CreateSnapshotRequest) (*hoapi.CreateSnapshotResponse, error) {
184+
return &hoapi.CreateSnapshotResponse{SnapshotID: c.randomString()}, nil
185+
}
186+
187+
func (c *FakeHostOrchestratorClient) Powerbtn(groupName, instanceName string) error {
188+
return nil
189+
}
190+
191+
func (c *FakeHostOrchestratorClient) DeleteSnapshot(string) error {
192+
return nil
193+
}
194+
195+
func (c *FakeHostOrchestratorClient) CreateUploadDir() (string, error) {
196+
return c.randomString(), nil
197+
}
198+
199+
func (c *FakeHostOrchestratorClient) UploadFile(uploadDir string, filename string) error {
200+
return nil
201+
}
202+
203+
func (c *FakeHostOrchestratorClient) UploadFileWithOptions(uploadDir string, filename string, options UploadOptions) error {
204+
return nil
205+
}
206+
207+
func (c *FakeHostOrchestratorClient) ExtractFile(uploadDir string, filename string) (*hoapi.Operation, error) {
208+
op, ch := c.newFakeOperation()
209+
go func() { ch <- []byte("done") }()
210+
return op, nil
211+
}
212+
213+
func (c *FakeHostOrchestratorClient) createFakeCVDs(total int) ([]*hoapi.CVD, error) {
214+
cvds := []*hoapi.CVD{}
215+
for cnt := 0; cnt < total; cnt++ {
216+
id := c.unusedCVDId()
217+
cvd := &hoapi.CVD{
218+
Group: fmt.Sprintf("cvd-%d", id),
219+
Name: fmt.Sprintf("%d", id),
220+
Status: "Running",
221+
Displays: []string{"720 x 1280 (320)"},
222+
WebRTCDeviceID: fmt.Sprintf("cvd-%d-%d", id, id),
223+
ADBSerial: fmt.Sprintf("0.0.0.0:%d", 6520+id-1),
224+
}
225+
c.cvds[id] = cvd
226+
cvds = append(cvds, cvd)
227+
}
228+
return cvds, nil
229+
}
230+
231+
func (c *FakeHostOrchestratorClient) createFakeEnvironment(config map[string]interface{}) ([]*hoapi.CVD, error) {
232+
instancesAny, hasInstances := config["instances"]
233+
if !hasInstances {
234+
return nil, fmt.Errorf("no instances in config")
235+
}
236+
instances, instancesIsArray := instancesAny.([]interface{})
237+
if !instancesIsArray {
238+
return nil, fmt.Errorf("instances is not an array")
239+
}
240+
cvds, _ := c.createFakeCVDs(len(instances))
241+
// Use the default one if not provided in the config
242+
groupName := cvds[0].Group
243+
if commonAny, hasCommon := config["common"]; hasCommon {
244+
if common, commonIsMap := commonAny.(map[string]interface{}); commonIsMap {
245+
if nameAny, hasName := common["group_name"]; hasName {
246+
if name, nameIsString := nameAny.(string); nameIsString {
247+
groupName = name
248+
}
249+
}
250+
}
251+
}
252+
253+
for i := 0; i < len(cvds); i++ {
254+
cvds[i].Group = groupName
255+
if instance, instanceIsMap := instances[i].(map[string]interface{}); instanceIsMap {
256+
if nameAny, hasName := instance["name"]; hasName {
257+
if name, nameIsStr := nameAny.(string); nameIsStr {
258+
cvds[i].Name = name
259+
}
260+
}
261+
}
262+
}
263+
264+
return cvds, nil
265+
}
266+
267+
func (c *FakeHostOrchestratorClient) unusedCVDId() int {
268+
id := 1
269+
for {
270+
if _, inUse := c.cvds[id]; !inUse {
271+
return id
272+
}
273+
id++
274+
}
275+
}
276+
277+
func (c *FakeHostOrchestratorClient) newFakeOperation() (*hoapi.Operation, chan []byte) {
278+
ch := make(chan []byte)
279+
op := &hoapi.Operation{
280+
Name: fmt.Sprintf("%d", len(c.operations)),
281+
Done: false,
282+
}
283+
c.operations[op.Name] = ch
284+
return op, ch
285+
}
286+
287+
func (c *FakeHostOrchestratorClient) randomString() string {
288+
return fmt.Sprint(time.Now().UnixNano())
289+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package libhoclient
16+
17+
import (
18+
"testing"
19+
)
20+
21+
func TestFakeHOClientImplementsInterface(t *testing.T) {
22+
// Make sure FakeHostOrchestratorClient implements the HostOrchestratorClient interface
23+
acceptHostOrchestratorClient := func(HostOrchestratorClient) {}
24+
acceptHostOrchestratorClient(NewFakeHostOrchestratorClient())
25+
}

0 commit comments

Comments
 (0)