initial commit
commit
920634d11b
|
@ -0,0 +1,128 @@
|
|||
# cmg-ws202425
|
||||
|
||||
Cloud-native Microservices mit Go - WS 2024/25
|
||||
|
||||
# Green Compute Load Shifting (GCLS)
|
||||
|
||||
![green compute load shifting](doc/overview.png)
|
||||
|
||||
## Functional Requirements
|
||||
|
||||
1. Consumers can create new computation jobs. A container image along with a set of environment variables defines the computation job.
|
||||
|
||||
2. Consumers can get the results of a compute job. The result consists of the state, the standard output produced by the job, and an estimate of the amount of CO2 equivalent that has been been emitted while executing the job.
|
||||
|
||||
3. Providers can offer computing capabilities and run assigned jobs
|
||||
|
||||
4. Jobs are scheduled to minimize runtime and carbon footprint
|
||||
|
||||
## Quality Requirements - Functional Suitability
|
||||
|
||||
### Functional Correctness
|
||||
|
||||
The error margin for estimating carbon emission shall be $\frac{+}{-} 10\%$
|
||||
|
||||
### Functional Appropriateness
|
||||
|
||||
In each week, the system shall reduce the carbon emissions of running compute jobs by at least $50\%$ compared to running the jobs in the client region
|
||||
|
||||
## Quality Requirements - Performance Efficiency
|
||||
|
||||
### Time behavior
|
||||
|
||||
$95\%$ of all backend requests that originate from a user must be handled in less than $50 ms$
|
||||
|
||||
### Capacity
|
||||
|
||||
The system must be able to handle up to 10000 jobs per day.
|
||||
|
||||
### Resource utilization
|
||||
|
||||
- The CPU and Memory consumption of all jobs running on a worker must not exceed $50\%$ of the available resources of that worker.
|
||||
|
||||
- **The overall cloud provider costs must not exceed the value of the obtained Google Cloud Education Credits**
|
||||
|
||||
## Quality Requirements - Compatibility
|
||||
|
||||
### Interoperability
|
||||
|
||||
The system must support Docker container images that are available on [the docker hub](https://hub.docker.com/)
|
||||
|
||||
## Quality Requirements - Reliability
|
||||
|
||||
### Availability
|
||||
|
||||
In each 24h timeframe, the request success rate must be at least $95\%$
|
||||
|
||||
### Fault tolerance
|
||||
|
||||
The system must not reject requests in case the carbon intensity provider is temporarily not available.
|
||||
|
||||
## Quality Requirements - Security
|
||||
|
||||
### Authenticity
|
||||
|
||||
All inter service communication must be authenticated and authorized (Zero Trust)
|
||||
|
||||
### Confidentiality
|
||||
|
||||
- Compute Consumers and Compute Providers must ony be able to access job information assigned to them
|
||||
|
||||
## Quality Requirements - Maintainability
|
||||
|
||||
### Reusability
|
||||
|
||||
Cross cutting concerns shall be implemented consistently in all services
|
||||
|
||||
### Testability
|
||||
|
||||
- The business code must be testable in isolation
|
||||
- The unit test coverage of the business code must be at least $80\%$
|
||||
|
||||
## Quality Requirements - Flexibility
|
||||
|
||||
### Replaceability
|
||||
|
||||
- The cloud provider must be replaceable without changing the business code
|
||||
- The carbon intensity provider must be replaceable without redeploying more than one service
|
||||
|
||||
## Boundary Conditions - Architecure
|
||||
|
||||
1. The predefined microservices architecture must be followed (see next slides)
|
||||
2. All services/clients must be implemented using the Go Programming Language
|
||||
3. Each microservice needs to adhere to the [ports & adapters](https://en.wikipedia.org/wiki/Hexagonal_architecture_(software)) structure
|
||||
4. All services need to be stateless
|
||||
5. All synchronous communication shall be handled using REST APIs
|
||||
6. APIs shall be idempotent, breaking changes shall be avoided
|
||||
7. Each service shall be containerized
|
||||
|
||||
## Boundary Conditions - Development
|
||||
|
||||
1. A single monorepo shall be used for all code <https://gitty.informatik.hs-mannheim.de/steger/cmg-ws202425.git>
|
||||
2. All Infrastructure shall be defined as code (IaC) using [terraform](https://www.terraform.io/)
|
||||
3. All features must be implemented in short lived feature branches
|
||||
4. The usage of 3rd party packages (other than from the Go standard library) must be approved
|
||||
5. All PRs must be reviewed by at least one other team member before a feature branch can be merged to main
|
||||
6. All tests must be passed before a feature branch can be merged to main
|
||||
|
||||
## Boundary Conditions - Security
|
||||
|
||||
1. JWTs shall be used for all requests originating from a client
|
||||
2. basic auth shall be used for all other communication
|
||||
3. secrets must not be stored in the repository
|
||||
|
||||
## Boundary Conditions - Deployment
|
||||
|
||||
1. The entrire system shall be deployed to the [Google cloud platform](https://console.cloud.google.com/). The gcp project id is `cmg-ws202425`.
|
||||
2. **The overall provider costs must not exceed the value of the obtained Google Cloud Education Credits**
|
||||
3. All services shall be deployed to a CaaS or PaaS offering in a single GCP cloud environment
|
||||
4. Each service has its own CI/CD pipeline
|
||||
5. Deployments are done independetly for each service with zero downtime (Rolling updates)
|
||||
|
||||
## Boundary Conditions - Operations
|
||||
|
||||
1. All logs shall be written to standard output
|
||||
2. Business, Application, and Infrastructure level metrics shall be collected by [prometheus](https://prometheus.io/).
|
||||
3. All requests shall be traced using [jaeger](https://www.jaegertracing.io/)
|
||||
4. each service must provide health probes for readiness and liveness
|
||||
5. each service must terminate upon receiving the SIGTERM signal within 2 seconds.
|
|
@ -0,0 +1,13 @@
|
|||
# Entity Service
|
||||
|
||||
The entity service is an example service that demonstrates the folder structure of a microservice following the ports & adapters architecture.
|
||||
|
||||
> **WARNING**
|
||||
> The implementation is in an early stage. Many things are still missing. Use with care.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
curl -X PUT -d '{ "Id": "34", "IntProp" : 23, "StringProp": "test" }' localhost:8080/entity
|
||||
curl localhost:8080/entity/34
|
||||
```
|
|
@ -0,0 +1,33 @@
|
|||
package jwt_store
|
||||
|
||||
import (
|
||||
"gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/cli/consumer-cli/ports"
|
||||
)
|
||||
|
||||
type JWTStore struct {
|
||||
//Gespeichert als Bytearray um manuelles überschreieben zu ermöglichen
|
||||
jwt []byte
|
||||
}
|
||||
|
||||
func NewJWTStore() *JWTStore {
|
||||
return &JWTStore{}
|
||||
}
|
||||
|
||||
func (s *JWTStore) GetJWT() string {
|
||||
return string(s.jwt)
|
||||
}
|
||||
|
||||
func (s *JWTStore) SetJWT(jwt string) {
|
||||
s.jwt = []byte(jwt)
|
||||
}
|
||||
|
||||
func (s *JWTStore) DeleteJWT() {
|
||||
|
||||
for i := range s.jwt {
|
||||
s.jwt[i] = 0
|
||||
}
|
||||
|
||||
s.jwt = nil
|
||||
}
|
||||
|
||||
var _ ports.JWTStore = (*JWTStore)(nil) // Check if the Store struct implements the Communication interface
|
|
@ -0,0 +1,25 @@
|
|||
package jwt_store
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestJWTStore(t *testing.T) {
|
||||
store := NewJWTStore()
|
||||
|
||||
if store.GetJWT() != "" {
|
||||
t.Error("Expected empty jwt, but got", store.GetJWT())
|
||||
}
|
||||
|
||||
store.SetJWT("mySecret")
|
||||
|
||||
if store.GetJWT() != "mySecret" {
|
||||
t.Error("Expected jwt to be set, but got", store.GetJWT())
|
||||
}
|
||||
|
||||
store.DeleteJWT()
|
||||
|
||||
if store.GetJWT() != "" {
|
||||
t.Error("Expected jwt to be deleted, but got", store.GetJWT())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
package rest_client_mock
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/cli/consumer-cli/ports"
|
||||
)
|
||||
|
||||
type MockRestClient struct {
|
||||
jobs map[uuid.UUID]ports.GetJobDto
|
||||
}
|
||||
|
||||
func NewMockRestClient() *MockRestClient {
|
||||
return &MockRestClient{jobs: make(map[uuid.UUID]ports.GetJobDto)}
|
||||
}
|
||||
|
||||
func (d *MockRestClient) GetJob(id string) (*ports.GetJobDto, error) {
|
||||
val, ok := d.jobs[uuid.MustParse(id)]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("job %s not found", id)
|
||||
}
|
||||
return &val, nil
|
||||
}
|
||||
|
||||
func (d *MockRestClient) GetJobs() ([]ports.GetJobDto, error) {
|
||||
jobs := []ports.GetJobDto{}
|
||||
for _, v := range d.jobs {
|
||||
jobs = append(jobs, v)
|
||||
}
|
||||
return jobs, nil
|
||||
}
|
||||
|
||||
func (d *MockRestClient) CreateJobDto(dto ports.CreateJobDto) error {
|
||||
job := dto.Job
|
||||
id := uuid.New()
|
||||
d.jobs[id] = ports.GetJobDto{
|
||||
Id: id.String(),
|
||||
Name: job.Name,
|
||||
ConsumerId: "",
|
||||
ImageName: job.ImageName,
|
||||
EnvironmentVariables: job.EnvironmentVariables,
|
||||
Status: "",
|
||||
StandardOutput: "",
|
||||
CreatedAt: 0,
|
||||
StartedAt: 0,
|
||||
FinishedAt: 0,
|
||||
ConsumerLongitude: job.ConsumerLongitude,
|
||||
ConsumerLatitude: job.ConsumerLatitude,
|
||||
Co2EquivalentEmissionConsumer: 0,
|
||||
EstimatedCo2Equivalent: 0,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *MockRestClient) Login(dto ports.LoginDto) (*ports.LoginResponseDto, error) {
|
||||
return &ports.LoginResponseDto{
|
||||
Token: "aaaaaa",
|
||||
}, nil
|
||||
}
|
||||
|
||||
type MockLocationStore struct {
|
||||
geolocation *ports.Geolocation
|
||||
}
|
||||
|
||||
func (d *MockLocationStore) GetLocation() (*ports.Geolocation, error) {
|
||||
if d.geolocation == nil {
|
||||
return nil, fmt.Errorf("no geolocation store")
|
||||
}
|
||||
return d.geolocation, nil
|
||||
}
|
||||
|
||||
func (d *MockLocationStore) SetLocation(latitude float64, longitude float64) error {
|
||||
d.geolocation = &ports.Geolocation{Latitude: latitude, Longitude: longitude}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
package rest_client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/cli/consumer-cli/ports"
|
||||
)
|
||||
|
||||
const serverPort = 8080
|
||||
|
||||
var requestURL = fmt.Sprintf("http://localhost:%d/", serverPort)
|
||||
|
||||
type RestClient struct {
|
||||
}
|
||||
|
||||
func NewRestClient() *RestClient {
|
||||
h := RestClient{}
|
||||
return &h
|
||||
}
|
||||
|
||||
func (r *RestClient) GetJob(id string) (*ports.GetJobDto, error) {
|
||||
var data *ports.GetJobDto
|
||||
data = &ports.GetJobDto{}
|
||||
|
||||
res, err := http.Get(requestURL + "job/" + id)
|
||||
if err != nil {
|
||||
fmt.Printf("Error making http request: %s\n", err)
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
fmt.Println("Error reading Response Body:", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
fmt.Println("Error parsing JSON:", err)
|
||||
return nil, err
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (r *RestClient) GetJobs() ([]ports.GetJobDto, error) {
|
||||
var data []ports.GetJobDto
|
||||
|
||||
res, err := http.Get(requestURL + "job")
|
||||
if err != nil {
|
||||
fmt.Printf("Error making http request: %s\n", err)
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
fmt.Println("Error reading Response Body:", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
fmt.Println("Error parsing JSON:", err)
|
||||
return nil, err
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (r *RestClient) CreateJobDto(dto ports.CreateJobDto) error {
|
||||
jsonBody, err := json.Marshal(dto)
|
||||
res, err := http.Post(requestURL+"job", "application/json", bytes.NewBuffer(jsonBody))
|
||||
if err != nil {
|
||||
fmt.Printf("Error making http request: %s\n", err)
|
||||
return err
|
||||
}
|
||||
if res.StatusCode != http.StatusOK {
|
||||
fmt.Printf("Error making http request: %s\n", res.Status)
|
||||
return err
|
||||
}
|
||||
fmt.Println("Success")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *RestClient) Login(dto ports.LoginDto) (*ports.LoginResponseDto, error) {
|
||||
var data *ports.LoginResponseDto
|
||||
data = &ports.LoginResponseDto{}
|
||||
|
||||
jsonBody, err := json.Marshal(dto)
|
||||
res, err := http.Post(requestURL+"login", "application/json", bytes.NewBuffer(jsonBody))
|
||||
if err != nil {
|
||||
fmt.Printf("Error making http request: %s\n", err)
|
||||
return nil, err
|
||||
}
|
||||
if res.StatusCode != http.StatusOK {
|
||||
fmt.Printf("Error making http request: %s\n", res.Status)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer res.Body.Close()
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
fmt.Println("Error reading Response Body:", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
fmt.Println("Error parsing JSON:", err)
|
||||
return nil, err
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
var _ ports.RestClient = (*RestClient)(nil) // Check if the Store struct implements the Communication interface
|
|
@ -0,0 +1,109 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/cli/consumer-cli/ports"
|
||||
)
|
||||
|
||||
type CLI struct {
|
||||
commandHandler ports.Api
|
||||
}
|
||||
|
||||
func NewCli(commandHandler ports.Api) *CLI {
|
||||
return &CLI{commandHandler: commandHandler}
|
||||
}
|
||||
|
||||
func (c *CLI) Scan(input string) {
|
||||
commandParser := CommandParser{}
|
||||
command, err := commandParser.ParseCommand(input)
|
||||
if err != nil {
|
||||
fmt.Printf("%s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
switch cmd := command.(type) {
|
||||
case *ports.CreateJobCommand:
|
||||
c.onCreateJobCommand(*cmd)
|
||||
case *ports.GetAllJobsCommand:
|
||||
c.onGetAllJobsCommand(*cmd)
|
||||
case *ports.GetJobByIdCommand:
|
||||
c.onGetJobByIdCommand(*cmd)
|
||||
case *ports.LoginCommand:
|
||||
c.onLoginCommand(*cmd)
|
||||
case *ports.GetConsumerLocationCommand:
|
||||
c.onGetConsumerLocationCommand(*cmd)
|
||||
case *ports.SetConsumerLocationCommand:
|
||||
c.onSetConsumerLocationCommand(*cmd)
|
||||
case *ports.HelpCommand:
|
||||
c.onHelpCommand(*cmd)
|
||||
default:
|
||||
fmt.Println("Please specify a valid command")
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CLI) onCreateJobCommand(cmd ports.CreateJobCommand) {
|
||||
result, err := c.commandHandler.HandleCreateJob(cmd)
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %s\n", err)
|
||||
} else {
|
||||
fmt.Printf("Created Job: %+v\n", result)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CLI) onGetAllJobsCommand(cmd ports.GetAllJobsCommand) {
|
||||
result, err := c.commandHandler.HandleGetAllJobs(cmd)
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %s\n", err)
|
||||
} else {
|
||||
for _, job := range result {
|
||||
c.printJob(job)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CLI) onGetJobByIdCommand(cmd ports.GetJobByIdCommand) {
|
||||
result, err := c.commandHandler.HandleGetJobById(cmd)
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %s\n", err)
|
||||
} else {
|
||||
c.printJob(*result)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CLI) onLoginCommand(cmd ports.LoginCommand) {
|
||||
_, err := c.commandHandler.HandleLogin(cmd)
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %s\n", err)
|
||||
} else {
|
||||
fmt.Printf("Successfully loged in\n")
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CLI) onGetConsumerLocationCommand(cmd ports.GetConsumerLocationCommand) {
|
||||
result, err := c.commandHandler.HandleGetConsumerLocation(cmd)
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %s\n", err)
|
||||
} else {
|
||||
fmt.Printf("Latitude: %f\nLongitude: %f\n", result.Latitude, result.Longitude)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CLI) onSetConsumerLocationCommand(cmd ports.SetConsumerLocationCommand) {
|
||||
err := c.commandHandler.HandleSetConsumerLocation(cmd)
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %s\n", err)
|
||||
} else {
|
||||
fmt.Println("Successfully set location")
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CLI) onHelpCommand(cmd ports.HelpCommand) {
|
||||
result := c.commandHandler.HandleHelp(cmd)
|
||||
fmt.Println(result)
|
||||
}
|
||||
|
||||
func (c *CLI) printJob(dto ports.GetJobDto) {
|
||||
fmt.Printf("ID: %s\nName: %s\nImageName: %s\nEnviromental Variables: %s\nStatus: %s\nStandard Output: %s\nCreated at: %d\nStarted at: %d\nFinished at: %d\nConsumer Latitude: %f\nConsumer Longitude: %f\nCo2 Equivalent Emission Consumer: %f\nEstimated Co2 Equvaelnt: %f\n", dto.Id, dto.Name, dto.ImageName, dto.EnvironmentVariables, dto.Status, dto.StandardOutput, dto.CreatedAt, dto.StartedAt, dto.FinishedAt, dto.ConsumerLatitude, dto.ConsumerLongitude, dto.Co2EquivalentEmissionConsumer, dto.EstimatedCo2Equivalent)
|
||||
fmt.Println()
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/cli/consumer-cli/ports"
|
||||
)
|
||||
|
||||
type MockCommandHandler struct {
|
||||
handleCreateJobFunc func(command ports.CreateJobCommand) (*ports.BaseJob, error)
|
||||
handleGetJobByIdFunc func(command ports.GetJobByIdCommand) (*ports.GetJobDto, error)
|
||||
handleGetAllJobsFunc func(command ports.GetAllJobsCommand) ([]ports.GetJobDto, error)
|
||||
handleLoginFunc func(command ports.LoginCommand) (*ports.LoginResponseDto, error)
|
||||
handleHelpFunc func(command ports.HelpCommand) string
|
||||
handleSetConsumerLocationFunc func(command ports.SetConsumerLocationCommand) error
|
||||
handleGetConsumerLocationFunc func(command ports.GetConsumerLocationCommand) (*ports.Geolocation, error)
|
||||
}
|
||||
|
||||
func (m *MockCommandHandler) HandleCreateJob(command ports.CreateJobCommand) (*ports.BaseJob, error) {
|
||||
return m.handleCreateJobFunc(command)
|
||||
}
|
||||
|
||||
func (m *MockCommandHandler) HandleGetJobById(command ports.GetJobByIdCommand) (*ports.GetJobDto, error) {
|
||||
return m.handleGetJobByIdFunc(command)
|
||||
}
|
||||
|
||||
func (m *MockCommandHandler) HandleGetAllJobs(command ports.GetAllJobsCommand) ([]ports.GetJobDto, error) {
|
||||
return m.handleGetAllJobsFunc(command)
|
||||
}
|
||||
|
||||
func (m *MockCommandHandler) HandleLogin(command ports.LoginCommand) (*ports.LoginResponseDto, error) {
|
||||
return m.handleLoginFunc(command)
|
||||
}
|
||||
|
||||
func (m *MockCommandHandler) HandleHelp(command ports.HelpCommand) string {
|
||||
return m.handleHelpFunc(command)
|
||||
}
|
||||
|
||||
func (m *MockCommandHandler) HandleSetConsumerLocation(command ports.SetConsumerLocationCommand) error {
|
||||
return m.handleSetConsumerLocationFunc(command)
|
||||
}
|
||||
|
||||
func (m *MockCommandHandler) HandleGetConsumerLocation(command ports.GetConsumerLocationCommand) (*ports.Geolocation, error) {
|
||||
return m.handleGetConsumerLocationFunc(command)
|
||||
}
|
||||
|
||||
func TestCLI(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expectedFunction string
|
||||
}{
|
||||
{
|
||||
name: "Create Job Command",
|
||||
input: `job create "myJob" "myImage" "env1:value1" "env2:value2"`,
|
||||
expectedFunction: "HandleCreateJob",
|
||||
},
|
||||
{
|
||||
name: "Get All Jobs Command",
|
||||
input: `job get`,
|
||||
expectedFunction: "HandleGetAllJobs",
|
||||
},
|
||||
{
|
||||
name: "Get Job By ID Command",
|
||||
input: `job get 123`,
|
||||
expectedFunction: "HandleGetJobById",
|
||||
},
|
||||
{
|
||||
name: "Login Command",
|
||||
input: `login "username" "password"`,
|
||||
expectedFunction: "HandleLogin",
|
||||
},
|
||||
{
|
||||
name: "Get Consumer Location Command",
|
||||
input: `location get`,
|
||||
expectedFunction: "HandleGetConsumerLocation",
|
||||
},
|
||||
{
|
||||
name: "Set Consumer Location Command",
|
||||
input: `location set 37.7749 -122.4194`,
|
||||
expectedFunction: "HandleSetConsumerLocation",
|
||||
},
|
||||
{
|
||||
name: "Help Command",
|
||||
input: `help`,
|
||||
expectedFunction: "HandleHelp",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockCommandHandler := &MockCommandHandler{
|
||||
handleCreateJobFunc: func(command ports.CreateJobCommand) (*ports.BaseJob, error) {
|
||||
if tt.expectedFunction != "HandleCreateJob" {
|
||||
t.Errorf("Unexpected function call: HandleCreateJob")
|
||||
}
|
||||
return nil, errors.New("not implemented")
|
||||
},
|
||||
handleGetJobByIdFunc: func(command ports.GetJobByIdCommand) (*ports.GetJobDto, error) {
|
||||
if tt.expectedFunction != "HandleGetJobById" {
|
||||
t.Errorf("Unexpected function call: HandleGetJobById")
|
||||
}
|
||||
return nil, errors.New("not implemented")
|
||||
},
|
||||
handleGetAllJobsFunc: func(command ports.GetAllJobsCommand) ([]ports.GetJobDto, error) {
|
||||
if tt.expectedFunction != "HandleGetAllJobs" {
|
||||
t.Errorf("Unexpected function call: HandleGetAllJobs")
|
||||
}
|
||||
return nil, errors.New("not implemented")
|
||||
},
|
||||
handleLoginFunc: func(command ports.LoginCommand) (*ports.LoginResponseDto, error) {
|
||||
if tt.expectedFunction != "HandleLogin" {
|
||||
t.Errorf("Unexpected function call: HandleLogin")
|
||||
}
|
||||
return nil, errors.New("not implemented")
|
||||
},
|
||||
handleHelpFunc: func(command ports.HelpCommand) string {
|
||||
if tt.expectedFunction != "HandleHelp" {
|
||||
t.Errorf("Unexpected function call: HandleHelp")
|
||||
}
|
||||
return ""
|
||||
},
|
||||
handleSetConsumerLocationFunc: func(command ports.SetConsumerLocationCommand) error {
|
||||
if tt.expectedFunction != "HandleSetConsumerLocation" {
|
||||
t.Errorf("Unexpected function call: HandleSetConsumerLocation")
|
||||
}
|
||||
return errors.New("not implemented")
|
||||
},
|
||||
handleGetConsumerLocationFunc: func(command ports.GetConsumerLocationCommand) (*ports.Geolocation, error) {
|
||||
if tt.expectedFunction != "HandleGetConsumerLocation" {
|
||||
t.Errorf("Unexpected function call: HandleGetConsumerLocation")
|
||||
}
|
||||
return nil, errors.New("not implemented")
|
||||
},
|
||||
}
|
||||
|
||||
cli := NewCli(mockCommandHandler)
|
||||
cli.Scan(tt.input)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
location_store "gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/cli/consumer-cli/core/location-store"
|
||||
"gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/cli/consumer-cli/ports"
|
||||
)
|
||||
|
||||
type CommandHandler struct {
|
||||
restClient ports.RestClient
|
||||
locationStore location_store.LocationStore
|
||||
}
|
||||
|
||||
var _ ports.Api = (*CommandHandler)(nil)
|
||||
|
||||
func NewCommandHandler(restCl ports.RestClient, locationSt location_store.LocationStore) *CommandHandler {
|
||||
return &CommandHandler{restClient: restCl, locationStore: locationSt}
|
||||
}
|
||||
|
||||
func (h *CommandHandler) HandleCreateJob(command ports.CreateJobCommand) (*ports.BaseJob, error) {
|
||||
location, err := h.locationStore.GetLocation()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to read location: %w", err)
|
||||
|
||||
}
|
||||
|
||||
createJobDto := ports.CreateJobDto{
|
||||
Job: ports.BaseJob{
|
||||
Name: command.JobName,
|
||||
ImageName: command.ImageName,
|
||||
EnvironmentVariables: command.EnvironmentVariables,
|
||||
ConsumerLongitude: location.Longitude,
|
||||
ConsumerLatitude: location.Latitude,
|
||||
},
|
||||
}
|
||||
err = h.restClient.CreateJobDto(createJobDto)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
} else {
|
||||
return &createJobDto.Job, nil
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (h *CommandHandler) HandleGetJobById(command ports.GetJobByIdCommand) (*ports.GetJobDto, error) {
|
||||
dto, err := h.restClient.GetJob(command.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
} else {
|
||||
return dto, nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (h *CommandHandler) HandleGetAllJobs(command ports.GetAllJobsCommand) ([]ports.GetJobDto, error) {
|
||||
joblist, err := h.restClient.GetJobs()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
} else {
|
||||
return joblist, nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (h *CommandHandler) HandleLogin(command ports.LoginCommand) (*ports.LoginResponseDto, error) {
|
||||
loginDto := ports.LoginDto{
|
||||
Credentials: ports.Credentials{
|
||||
Username: command.UserName,
|
||||
Password: command.Password,
|
||||
},
|
||||
}
|
||||
loginResponse, err := h.restClient.Login(loginDto)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
} else {
|
||||
return loginResponse, nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (h *CommandHandler) HandleHelp(command ports.HelpCommand) string {
|
||||
return "Test"
|
||||
|
||||
}
|
||||
|
||||
func (h *CommandHandler) HandleSetConsumerLocation(command ports.SetConsumerLocationCommand) error {
|
||||
err := h.locationStore.SetLocation(command.Latitude, command.Longitude)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read Location: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *CommandHandler) HandleGetConsumerLocation(command ports.GetConsumerLocationCommand) (*ports.Geolocation, error) {
|
||||
geolocation, err := h.locationStore.GetLocation()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to read Location: %w", err)
|
||||
}
|
||||
return geolocation, nil
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
rest_client_mock "gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/cli/consumer-cli/adapter/mocks/rest_client_mock"
|
||||
"gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/cli/consumer-cli/ports"
|
||||
)
|
||||
|
||||
func TestCommandHandler_HandleCreateJob(t *testing.T) {
|
||||
handler := NewCommandHandler(rest_client_mock.NewMockRestClient(), &rest_client_mock.MockLocationStore{})
|
||||
|
||||
commandBeforeLocationSet := ports.CreateJobCommand{
|
||||
JobName: "",
|
||||
ImageName: "",
|
||||
EnvironmentVariables: nil,
|
||||
}
|
||||
a, err := handler.HandleCreateJob(commandBeforeLocationSet)
|
||||
if err == nil {
|
||||
t.Error("Expected error, got nil ", a)
|
||||
}
|
||||
|
||||
setLocationCmd := ports.SetConsumerLocationCommand{
|
||||
Longitude: 0,
|
||||
Latitude: 0,
|
||||
}
|
||||
|
||||
err = handler.HandleSetConsumerLocation(setLocationCmd)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
commandAfterLocationSet := ports.CreateJobCommand{
|
||||
JobName: "",
|
||||
ImageName: "",
|
||||
EnvironmentVariables: nil,
|
||||
}
|
||||
|
||||
_, err = handler.HandleCreateJob(commandAfterLocationSet)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
jobs, err := handler.HandleGetAllJobs(ports.GetAllJobsCommand{})
|
||||
if len(jobs) != 1 {
|
||||
t.Error("Expected 1 job, got ", len(jobs))
|
||||
}
|
||||
id := jobs[0].Id
|
||||
|
||||
job, err := handler.HandleGetJobById(ports.GetJobByIdCommand{ID: id})
|
||||
if job == nil {
|
||||
t.Error("Expected job, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandHandler_HandleLogin(t *testing.T) {
|
||||
handler := NewCommandHandler(rest_client_mock.NewMockRestClient(), &rest_client_mock.MockLocationStore{})
|
||||
|
||||
_, err := handler.HandleLogin(ports.LoginCommand{
|
||||
UserName: "a",
|
||||
Password: "b",
|
||||
})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandHandler_HandleLocation(t *testing.T) {
|
||||
handler := NewCommandHandler(rest_client_mock.NewMockRestClient(), &rest_client_mock.MockLocationStore{})
|
||||
|
||||
_, err := handler.HandleGetConsumerLocation(ports.GetConsumerLocationCommand{})
|
||||
if err == nil {
|
||||
t.Error("Expected 'get location' to fail ")
|
||||
}
|
||||
|
||||
err = handler.HandleSetConsumerLocation(ports.SetConsumerLocationCommand{
|
||||
Longitude: 10,
|
||||
Latitude: -7,
|
||||
})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
geo, err := handler.HandleGetConsumerLocation(ports.GetConsumerLocationCommand{})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if geo.Longitude != 10 || geo.Latitude != -7 {
|
||||
t.Error("Wrong Coordinates")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandHandler_HandleHelp(t *testing.T) {
|
||||
handler := NewCommandHandler(rest_client_mock.NewMockRestClient(), &rest_client_mock.MockLocationStore{})
|
||||
|
||||
help := handler.HandleHelp(ports.HelpCommand{})
|
||||
if help == "" {
|
||||
t.Error("Expected help")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,153 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/cli/consumer-cli/ports"
|
||||
)
|
||||
|
||||
type CommandParser struct {
|
||||
}
|
||||
|
||||
const JOB_COMMAND_IDENTIFIER = "job"
|
||||
const JOB_CREATE_ACTION = "create"
|
||||
const JOB_GET_ACTION = "get"
|
||||
|
||||
const LOGIN_COMMAND_IDENTIFIER = "login"
|
||||
|
||||
const LOCATION_COMMAND_IDENTIFIER = "location"
|
||||
const LOCATION_GET_ACTION = "get"
|
||||
const LOCATION_SET_ACTION = "set"
|
||||
|
||||
const HELP_COMMAND_IDENTIFIER = "help"
|
||||
|
||||
func (c *CommandParser) ParseCommand(command string) (ports.Command, error) {
|
||||
command = strings.Replace(command, "\n", "", -1)
|
||||
command = strings.TrimSpace(command)
|
||||
|
||||
pattern := `"[^"]*"|\S+`
|
||||
re := regexp.MustCompile(pattern)
|
||||
matches := re.FindAllStringSubmatch(command, -1)
|
||||
commandParts, err := flattenAndRemoveQuotes(matches)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(commandParts) == 0 {
|
||||
return nil, errors.New("invalid command")
|
||||
}
|
||||
|
||||
commandIdentifier := commandParts[0]
|
||||
switch commandIdentifier {
|
||||
case JOB_COMMAND_IDENTIFIER:
|
||||
return extractJobCommand(commandParts)
|
||||
case LOGIN_COMMAND_IDENTIFIER:
|
||||
return extractLoginCommand(commandParts)
|
||||
case LOCATION_COMMAND_IDENTIFIER:
|
||||
return extractLocationCommand(commandParts)
|
||||
case HELP_COMMAND_IDENTIFIER:
|
||||
return &ports.HelpCommand{}, nil
|
||||
default:
|
||||
return nil, errors.New("unkonwn command")
|
||||
}
|
||||
}
|
||||
|
||||
func extractLocationCommand(commandParts []string) (ports.Command, error) {
|
||||
if len(commandParts) <= 1 {
|
||||
return nil, errors.New("invalid location command")
|
||||
}
|
||||
|
||||
action := commandParts[1]
|
||||
switch action {
|
||||
case LOCATION_GET_ACTION:
|
||||
return &ports.GetConsumerLocationCommand{}, nil
|
||||
case LOCATION_SET_ACTION:
|
||||
if len(commandParts) != 4 {
|
||||
return nil, errors.New("invalid set location command")
|
||||
}
|
||||
|
||||
latitude, err := strconv.ParseFloat(commandParts[2], 64)
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid latitude")
|
||||
}
|
||||
longitude, err := strconv.ParseFloat(commandParts[3], 64)
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid longitude")
|
||||
}
|
||||
|
||||
return &ports.SetConsumerLocationCommand{Longitude: longitude, Latitude: latitude}, nil
|
||||
default:
|
||||
return nil, errors.New("unknown location action")
|
||||
}
|
||||
}
|
||||
|
||||
func extractLoginCommand(commandParts []string) (ports.Command, error) {
|
||||
if len(commandParts) != 3 {
|
||||
return nil, errors.New("invalid login command")
|
||||
}
|
||||
|
||||
return &ports.LoginCommand{UserName: commandParts[1], Password: commandParts[2]}, nil
|
||||
}
|
||||
|
||||
func extractJobCommand(commandParts []string) (ports.Command, error) {
|
||||
commandPartsLen := len(commandParts)
|
||||
if commandPartsLen <= 1 {
|
||||
return nil, errors.New("invalid job command")
|
||||
}
|
||||
|
||||
action := commandParts[1]
|
||||
switch action {
|
||||
case JOB_CREATE_ACTION:
|
||||
if commandPartsLen <= 3 {
|
||||
return nil, errors.New("not enough arguments for create job command")
|
||||
}
|
||||
|
||||
return extractCreateJobCommand(commandParts[2:])
|
||||
case JOB_GET_ACTION:
|
||||
if commandPartsLen == 2 {
|
||||
return &ports.GetAllJobsCommand{}, nil
|
||||
} else if commandPartsLen == 3 {
|
||||
return &ports.GetJobByIdCommand{commandParts[2]}, nil
|
||||
}
|
||||
return nil, errors.New("too many arguments for get job command")
|
||||
default:
|
||||
return nil, errors.New("unknown job action")
|
||||
}
|
||||
}
|
||||
|
||||
func extractCreateJobCommand(args []string) (ports.Command, error) {
|
||||
jobName := args[0]
|
||||
imageName := args[1]
|
||||
|
||||
if len(args) == 2 {
|
||||
return &ports.CreateJobCommand{JobName: jobName, ImageName: imageName}, nil
|
||||
}
|
||||
|
||||
envVars := make(map[string]string)
|
||||
for _, keyValuePair := range args[2:] {
|
||||
parts := strings.SplitN(keyValuePair, ":", 2)
|
||||
if len(parts) != 2 {
|
||||
return nil, errors.New("invalid environment variable format")
|
||||
}
|
||||
key := parts[0]
|
||||
value := parts[1]
|
||||
envVars[key] = value
|
||||
}
|
||||
|
||||
return &ports.CreateJobCommand{JobName: jobName, ImageName: imageName, EnvironmentVariables: envVars}, nil
|
||||
}
|
||||
|
||||
func flattenAndRemoveQuotes(slices [][]string) ([]string, error) {
|
||||
var result []string
|
||||
for _, elements := range slices {
|
||||
if len(elements) != 1 {
|
||||
return nil, errors.New("something went wrong")
|
||||
}
|
||||
element := strings.ReplaceAll(elements[0], `"`, "")
|
||||
result = append(result, element)
|
||||
}
|
||||
return result, nil
|
||||
}
|
|
@ -0,0 +1,310 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/cli/consumer-cli/ports"
|
||||
)
|
||||
|
||||
func TestCreateJobCommand(t *testing.T) {
|
||||
commandParser := CommandParser{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
command string
|
||||
expectedCommand *ports.CreateJobCommand
|
||||
}{
|
||||
{
|
||||
name: "Arguments in double quotes",
|
||||
command: `job create "my test Job" "acc/myImage" "path:some/path/to/file" "user:Tony :Fallony"`,
|
||||
expectedCommand: &ports.CreateJobCommand{JobName: "my test Job", ImageName: "acc/myImage", EnvironmentVariables: map[string]string{"path": "some/path/to/file", "user": "Tony :Fallony"}},
|
||||
},
|
||||
{
|
||||
name: "Some arguments not in double quotes",
|
||||
command: `job create myJob "acc/myImage" path:some/path/to/file "user:Tony Fallony"`,
|
||||
expectedCommand: &ports.CreateJobCommand{JobName: "myJob", ImageName: "acc/myImage", EnvironmentVariables: map[string]string{"path": "some/path/to/file", "user": "Tony Fallony"}},
|
||||
},
|
||||
{
|
||||
name: "No environment variables",
|
||||
command: `job create myJob acc/myImage`,
|
||||
expectedCommand: &ports.CreateJobCommand{JobName: "myJob", ImageName: "acc/myImage"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := commandParser.ParseCommand(tt.command)
|
||||
if err != nil {
|
||||
t.Errorf("Error: %s", err)
|
||||
}
|
||||
|
||||
switch v := result.(type) {
|
||||
case *ports.CreateJobCommand:
|
||||
if v.JobName != tt.expectedCommand.JobName {
|
||||
t.Errorf("Wrong job name. Expected '%s' but got '%s'.", tt.expectedCommand.JobName, v.JobName)
|
||||
}
|
||||
if v.ImageName != tt.expectedCommand.ImageName {
|
||||
t.Errorf("Wrong image name. Expected '%s' but got '%s'.", tt.expectedCommand.ImageName, v.ImageName)
|
||||
}
|
||||
for key, expectedValue := range tt.expectedCommand.EnvironmentVariables {
|
||||
if value, ok := v.EnvironmentVariables[key]; !ok || value != expectedValue {
|
||||
t.Errorf("Wrong environment variable '%s'. Expected '%s' but got '%s'.", key, expectedValue, value)
|
||||
}
|
||||
}
|
||||
default:
|
||||
t.Errorf("Create job command is of wrong type: '%T'", result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAllJobsCommand(t *testing.T) {
|
||||
commandParser := CommandParser{}
|
||||
|
||||
command := `job get`
|
||||
|
||||
result, err := commandParser.ParseCommand(command)
|
||||
if err != nil {
|
||||
t.Errorf("Error: %s", err)
|
||||
}
|
||||
|
||||
switch result.(type) {
|
||||
case *ports.GetAllJobsCommand:
|
||||
if _, ok := result.(*ports.GetAllJobsCommand); !ok {
|
||||
t.Errorf("Expected GetAllJobsCommand but got '%T'", result)
|
||||
}
|
||||
default:
|
||||
t.Errorf("Get all jobs command is of wrong type: '%T'", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetJobByIdCommand(t *testing.T) {
|
||||
commandParser := CommandParser{}
|
||||
|
||||
command := `job get 123`
|
||||
expectedCommand := ports.GetJobByIdCommand{ID: "123"}
|
||||
|
||||
result, err := commandParser.ParseCommand(command)
|
||||
if err != nil {
|
||||
t.Errorf("Error: %s", err)
|
||||
}
|
||||
|
||||
switch v := result.(type) {
|
||||
case *ports.GetJobByIdCommand:
|
||||
if v.ID != expectedCommand.ID {
|
||||
t.Errorf("Wrong job ID. Expected '%s' but got '%s'.", expectedCommand.ID, v.ID)
|
||||
}
|
||||
default:
|
||||
t.Errorf("Get job by ID command is of wrong type: '%T'", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoginCommand(t *testing.T) {
|
||||
commandParser := CommandParser{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
command string
|
||||
expectedCommand ports.LoginCommand
|
||||
}{
|
||||
{
|
||||
name: "Arguments in double quotes",
|
||||
command: `login "username" "my secret password"`,
|
||||
expectedCommand: ports.LoginCommand{UserName: "username", Password: "my secret password"},
|
||||
},
|
||||
{
|
||||
name: "Some arguments not in double quotes",
|
||||
command: `login userName "password"`,
|
||||
expectedCommand: ports.LoginCommand{UserName: "userName", Password: "password"},
|
||||
},
|
||||
{
|
||||
name: "All arguments not in double quotes",
|
||||
command: `login username password`,
|
||||
expectedCommand: ports.LoginCommand{UserName: "username", Password: "password"},
|
||||
},
|
||||
{
|
||||
name: "Login with empty password",
|
||||
command: `login "username" ""`,
|
||||
expectedCommand: ports.LoginCommand{UserName: "username", Password: ""},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := commandParser.ParseCommand(tt.command)
|
||||
if err != nil {
|
||||
t.Errorf("Error: %s", err)
|
||||
}
|
||||
|
||||
switch v := result.(type) {
|
||||
case *ports.LoginCommand:
|
||||
if v.UserName != tt.expectedCommand.UserName {
|
||||
t.Errorf("Wrong username. Expected '%s' but got '%s'.", tt.expectedCommand.UserName, v.UserName)
|
||||
}
|
||||
if v.Password != tt.expectedCommand.Password {
|
||||
t.Errorf("Wrong password. Expected '%s' but got '%s'.", tt.expectedCommand.Password, v.Password)
|
||||
}
|
||||
default:
|
||||
t.Errorf("Login command is of wrong type: '%T'", result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetConsumerLocationCommand(t *testing.T) {
|
||||
commandParser := CommandParser{}
|
||||
|
||||
command := `location get`
|
||||
|
||||
result, err := commandParser.ParseCommand(command)
|
||||
if err != nil {
|
||||
t.Errorf("Error: %s", err)
|
||||
}
|
||||
|
||||
switch result.(type) {
|
||||
case *ports.GetConsumerLocationCommand:
|
||||
if _, ok := result.(*ports.GetConsumerLocationCommand); !ok {
|
||||
t.Errorf("Expected GetConsumerLocationCommand but got '%T'", result)
|
||||
}
|
||||
default:
|
||||
t.Errorf("Get consumer location command is of wrong type: '%T'", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetConsumerLocationCommand(t *testing.T) {
|
||||
commandParser := CommandParser{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
command string
|
||||
expectedCommand ports.SetConsumerLocationCommand
|
||||
}{
|
||||
{
|
||||
name: "Arguments with quotes",
|
||||
command: `location set "-122.4194" "37.7749"`,
|
||||
expectedCommand: ports.SetConsumerLocationCommand{Longitude: 37.7749, Latitude: -122.4194},
|
||||
},
|
||||
{
|
||||
name: "Some arguments with quotes",
|
||||
command: `location set "123" 0 `,
|
||||
expectedCommand: ports.SetConsumerLocationCommand{Longitude: 0, Latitude: 123},
|
||||
},
|
||||
{
|
||||
name: "No arguments with quotes",
|
||||
command: `location set -122.4194 -37.7749`,
|
||||
expectedCommand: ports.SetConsumerLocationCommand{Longitude: -37.7749, Latitude: -122.4194},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := commandParser.ParseCommand(tt.command)
|
||||
if err != nil {
|
||||
t.Errorf("Error: %s", err)
|
||||
}
|
||||
|
||||
switch v := result.(type) {
|
||||
case *ports.SetConsumerLocationCommand:
|
||||
if v.Longitude != tt.expectedCommand.Longitude {
|
||||
t.Errorf("Wrong longitude. Expected '%f' but got '%f'.", tt.expectedCommand.Longitude, v.Longitude)
|
||||
}
|
||||
if v.Latitude != tt.expectedCommand.Latitude {
|
||||
t.Errorf("Wrong latitude. Expected '%f' but got '%f'.", tt.expectedCommand.Latitude, v.Latitude)
|
||||
}
|
||||
default:
|
||||
t.Errorf("Set consumer location command is of wrong type: '%T'", result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHelpCommand(t *testing.T) {
|
||||
commandParser := CommandParser{}
|
||||
|
||||
command := `help`
|
||||
|
||||
result, err := commandParser.ParseCommand(command)
|
||||
if err != nil {
|
||||
t.Errorf("Error: %s", err)
|
||||
}
|
||||
|
||||
switch result.(type) {
|
||||
case *ports.HelpCommand:
|
||||
if _, ok := result.(*ports.HelpCommand); !ok {
|
||||
t.Errorf("Expected HelpCommand but got '%T'", result)
|
||||
}
|
||||
default:
|
||||
t.Errorf("Help command is of wrong type: '%T'", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidCommands(t *testing.T) {
|
||||
commandParser := CommandParser{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
command string
|
||||
}{
|
||||
{
|
||||
name: "Empty command",
|
||||
command: ``,
|
||||
},
|
||||
{
|
||||
name: "Unknown command",
|
||||
command: `unknown command`,
|
||||
},
|
||||
{
|
||||
name: "Job command with invalid action",
|
||||
command: `job unknown`,
|
||||
},
|
||||
{
|
||||
name: "Create job command with missing args",
|
||||
command: `job create`,
|
||||
},
|
||||
{
|
||||
name: "Create job command with missing image name",
|
||||
command: `job create myJob`,
|
||||
},
|
||||
{
|
||||
name: "Create job command with invalid envs",
|
||||
command: `job create myJob myImage "key1"`,
|
||||
},
|
||||
{
|
||||
name: "Invalid job get command with too many arguments",
|
||||
command: `job get 123 extra`,
|
||||
},
|
||||
{
|
||||
name: "Invalid location action",
|
||||
command: `location unknown`,
|
||||
},
|
||||
{
|
||||
name: "Invalid location set command with missing arguments",
|
||||
command: `location set 37.7749`,
|
||||
},
|
||||
{
|
||||
name: "Invalid location set command with non numeric longitude",
|
||||
command: `location set longi 37.7749`,
|
||||
},
|
||||
{
|
||||
name: "Invalid location set command with non numeric latitude",
|
||||
command: `location set 37.7749 lati`,
|
||||
},
|
||||
{
|
||||
name: "Invalid login command with missing arguments",
|
||||
command: `login`,
|
||||
},
|
||||
{
|
||||
name: "Invalid login command with missing password",
|
||||
command: `login username`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := commandParser.ParseCommand(tt.command)
|
||||
if err == nil {
|
||||
t.Errorf("Expected error for command '%s' but got none", tt.command)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
package location_store
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/cli/consumer-cli/ports"
|
||||
)
|
||||
|
||||
type locationStore struct {
|
||||
}
|
||||
|
||||
type LocationStore interface {
|
||||
GetLocation() (*ports.Geolocation, error)
|
||||
SetLocation(latitude float64, longitude float64) error
|
||||
}
|
||||
|
||||
func NewLocationStore() *locationStore {
|
||||
return &locationStore{}
|
||||
}
|
||||
|
||||
const filename = ".userlocation"
|
||||
|
||||
func (store *locationStore) GetLocation() (*ports.Geolocation, error) {
|
||||
geo, err := readFile()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return geo, nil
|
||||
}
|
||||
|
||||
func (store *locationStore) SetLocation(latitude float64, longitude float64) error {
|
||||
geo := ports.Geolocation{
|
||||
Latitude: latitude,
|
||||
Longitude: longitude,
|
||||
}
|
||||
err := writeToFile(geo)
|
||||
return err
|
||||
}
|
||||
|
||||
func readFile() (*ports.Geolocation, error) {
|
||||
exePath, err := os.Executable()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error finding executable path: %w", err)
|
||||
}
|
||||
configDir := filepath.Dir(exePath)
|
||||
configFilePath := filepath.Join(configDir, filename)
|
||||
|
||||
file, err := os.Open(configFilePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("location was not set")
|
||||
}
|
||||
defer file.Close()
|
||||
location := &ports.Geolocation{}
|
||||
decoder := json.NewDecoder(file)
|
||||
if err := decoder.Decode(location); err != nil {
|
||||
return nil, fmt.Errorf("error decoding file: %w", err)
|
||||
}
|
||||
return location, nil
|
||||
}
|
||||
|
||||
func writeToFile(location ports.Geolocation) error {
|
||||
exePath, err := os.Executable()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error finding executable path: %w", err)
|
||||
}
|
||||
configDir := filepath.Dir(exePath)
|
||||
configFilePath := filepath.Join(configDir, filename)
|
||||
|
||||
file, err := os.Create(configFilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error opening file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
encoder := json.NewEncoder(file)
|
||||
if err := encoder.Encode(location); err != nil {
|
||||
return fmt.Errorf("error encoding file: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
package location_store
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
_ "fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/cli/consumer-cli/ports"
|
||||
)
|
||||
|
||||
func TestSetLocation(t *testing.T) {
|
||||
store := NewLocationStore()
|
||||
|
||||
err := store.SetLocation(40.7128, -74.0060)
|
||||
if err != nil {
|
||||
t.Errorf("Error setting location: %v", err)
|
||||
}
|
||||
|
||||
exePath, _ := os.Executable()
|
||||
configFilePath := filepath.Join(filepath.Dir(exePath), filename)
|
||||
t.Log(configFilePath)
|
||||
file, err := os.Open(configFilePath)
|
||||
if err != nil {
|
||||
t.Fatalf("Error opening config file: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var savedLocation ports.Geolocation
|
||||
decoder := json.NewDecoder(file)
|
||||
if err := decoder.Decode(&savedLocation); err != nil {
|
||||
t.Fatalf("Error decoding saved location: %v", err)
|
||||
}
|
||||
|
||||
expectedLocation := ports.Geolocation{Latitude: 40.7128, Longitude: -74.0060}
|
||||
if savedLocation != expectedLocation {
|
||||
t.Errorf("Expected %v, got %v", expectedLocation, savedLocation)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetLocation(t *testing.T) {
|
||||
mockLocation := ports.Geolocation{Latitude: 51.5074, Longitude: -0.1278}
|
||||
|
||||
store := NewLocationStore()
|
||||
|
||||
err := store.SetLocation(mockLocation.Latitude, mockLocation.Longitude)
|
||||
if err != nil {
|
||||
t.Errorf("Error setting location: %v", err)
|
||||
}
|
||||
|
||||
location, err := store.GetLocation()
|
||||
if err != nil {
|
||||
t.Errorf("Error getting location: %v", err)
|
||||
}
|
||||
|
||||
if reflect.DeepEqual(location, mockLocation) {
|
||||
t.Errorf("Expected %v, got %v", mockLocation, *location)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
module gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/cli/consumer-cli
|
||||
|
||||
go 1.23.1
|
||||
|
||||
require github.com/google/uuid v1.6.0
|
|
@ -0,0 +1,2 @@
|
|||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
@ -0,0 +1,25 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
rest_client_mock "gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/cli/consumer-cli/adapter/mocks/rest_client_mock"
|
||||
cli "gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/cli/consumer-cli/core/cli"
|
||||
location_store "gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/cli/consumer-cli/core/location-store"
|
||||
)
|
||||
|
||||
func main() {
|
||||
restClient := rest_client_mock.NewMockRestClient()
|
||||
locationStore := location_store.NewLocationStore()
|
||||
commandHandler := cli.NewCommandHandler(restClient, locationStore)
|
||||
cli := cli.NewCli(commandHandler)
|
||||
|
||||
for true {
|
||||
fmt.Print("Input: ")
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
input, _ := reader.ReadString('\n')
|
||||
cli.Scan(input)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
package ports
|
||||
|
||||
type Api interface {
|
||||
HandleCreateJob(command CreateJobCommand) (*BaseJob, error)
|
||||
HandleGetJobById(command GetJobByIdCommand) (*GetJobDto, error)
|
||||
HandleGetAllJobs(command GetAllJobsCommand) ([]GetJobDto, error)
|
||||
HandleLogin(command LoginCommand) (*LoginResponseDto, error)
|
||||
HandleHelp(command HelpCommand) string
|
||||
HandleSetConsumerLocation(command SetConsumerLocationCommand) error
|
||||
HandleGetConsumerLocation(command GetConsumerLocationCommand) (*Geolocation, error)
|
||||
}
|
||||
|
||||
type Command interface {
|
||||
implementCommand()
|
||||
}
|
||||
|
||||
type CreateJobCommand struct {
|
||||
JobName string
|
||||
ImageName string
|
||||
EnvironmentVariables map[string]string
|
||||
}
|
||||
|
||||
type GetAllJobsCommand struct{}
|
||||
|
||||
type GetJobByIdCommand struct {
|
||||
ID string
|
||||
}
|
||||
|
||||
type LoginCommand struct {
|
||||
UserName string
|
||||
Password string
|
||||
}
|
||||
|
||||
type GetConsumerLocationCommand struct {
|
||||
}
|
||||
|
||||
type SetConsumerLocationCommand struct {
|
||||
Longitude float64
|
||||
Latitude float64
|
||||
}
|
||||
|
||||
type HelpCommand struct{}
|
||||
|
||||
func (c *CreateJobCommand) implementCommand() {}
|
||||
|
||||
func (c *GetAllJobsCommand) implementCommand() {}
|
||||
|
||||
func (c *GetJobByIdCommand) implementCommand() {}
|
||||
|
||||
func (c *LoginCommand) implementCommand() {}
|
||||
|
||||
func (c *GetConsumerLocationCommand) implementCommand() {}
|
||||
|
||||
func (c *SetConsumerLocationCommand) implementCommand() {}
|
||||
|
||||
func (c *HelpCommand) implementCommand() {}
|
|
@ -0,0 +1,7 @@
|
|||
package ports
|
||||
|
||||
type JWTStore interface {
|
||||
GetJWT() string
|
||||
SetJWT(jwt string)
|
||||
DeleteJWT()
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
package ports
|
||||
|
||||
type GetJobDto struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
ConsumerId string `json:"consumerId"`
|
||||
ImageName string `json:"imageName"`
|
||||
EnvironmentVariables map[string]string `json:"environmentVariables"`
|
||||
Status JobStatus `json:"status"`
|
||||
StandardOutput string `json:"standardOutput"`
|
||||
|
||||
CreatedAt int64 `json:"createdAt"`
|
||||
StartedAt int64 `json:"startedAt"`
|
||||
FinishedAt int64 `json:"finishedAt"`
|
||||
|
||||
ConsumerLongitude float64 `json:"consumerLongitude"`
|
||||
ConsumerLatitude float64 `json:"consumerLatitude"`
|
||||
Co2EquivalentEmissionConsumer float64 `json:"co2EquivalentEmissionConsumer"`
|
||||
|
||||
EstimatedCo2Equivalent float64 `json:"estimatedCo2Equivalent"`
|
||||
}
|
||||
|
||||
type JobStatus string
|
||||
|
||||
const (
|
||||
CREATED JobStatus = "CREATED" // job created, but not assigned to worker yet
|
||||
PENDING JobStatus = "PENDING" // assigned to worker, but not running yet
|
||||
RUNNING JobStatus = "RUNNING"
|
||||
FINISHED JobStatus = "FINISHED"
|
||||
FAILED JobStatus = "FAILED"
|
||||
)
|
||||
|
||||
type CreateJobDto struct {
|
||||
RequestId string `json:"requestId"`
|
||||
Job BaseJob `json:"job"`
|
||||
}
|
||||
|
||||
type LoginDto struct {
|
||||
RequestId string `json:"requestId"`
|
||||
Credentials Credentials `json:"credentials"`
|
||||
}
|
||||
|
||||
type LoginResponseDto struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
type BaseJob struct {
|
||||
Name string `json:"name"`
|
||||
ImageName string `json:"imageName"`
|
||||
EnvironmentVariables map[string]string `json:"environmentVariables"`
|
||||
ConsumerLongitude float64 `json:"consumerLongitude"`
|
||||
ConsumerLatitude float64 `json:"consumerLatitude"`
|
||||
}
|
||||
|
||||
type Credentials struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type Geolocation struct {
|
||||
Latitude float64 `json:"latitude"`
|
||||
Longitude float64 `json:"longitude"`
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package ports
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
var ErrEntityNotFound = errors.New("entity not found")
|
||||
|
||||
type RestClient interface {
|
||||
GetJob(id string) (*GetJobDto, error)
|
||||
GetJobs() ([]GetJobDto, error)
|
||||
CreateJobDto(dto CreateJobDto) error
|
||||
Login(dto LoginDto) (*LoginResponseDto, error)
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Command struct {
|
||||
Use string
|
||||
Description string
|
||||
Run func([]string)
|
||||
}
|
||||
|
||||
var commands = make(map[string]Command)
|
||||
|
||||
func Execute() {
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Println("Not enough arguments!")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
command, ok := commands[os.Args[1]]
|
||||
if !ok {
|
||||
fmt.Println("Unknown command!")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
command.Run(os.Args[2:])
|
||||
}
|
||||
|
||||
func RegisterCommand(command Command) {
|
||||
commands[command.Use] = command
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/cli/gcls-modulith/core"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterCommand(Command{
|
||||
Use: "gcls-modulith",
|
||||
Description: "A container image is passed, turned into a job and then executed.",
|
||||
Run: func(args []string) {
|
||||
if len(args) != 1 {
|
||||
fmt.Println("Invalid number of arguments!")
|
||||
return
|
||||
}
|
||||
|
||||
core.ExecuteComputeJob(args[0])
|
||||
},
|
||||
})
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
worker_client "gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/cli/gcls-modulith/services/gcls-worker-daemon/adapters/client-cli"
|
||||
worker_executor "gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/cli/gcls-modulith/services/gcls-worker-daemon/adapters/executor-cli"
|
||||
worker_core "gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/services/gcls-worker-daemon/core"
|
||||
worker_ports "gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/services/gcls-worker-daemon/ports"
|
||||
|
||||
worker_repo "gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/services/worker-registry/adapters/repo-in-memory"
|
||||
worker_registry_core "gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/services/worker-registry/core"
|
||||
|
||||
job_repo "gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/services/job/adapters/repo-in-memory"
|
||||
job_core "gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/services/job/core"
|
||||
job_ports "gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/services/job/ports"
|
||||
// job_scheduler_core "gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/services/job-scheduler/core"
|
||||
)
|
||||
|
||||
func ExecuteComputeJob(dockerImage string) {
|
||||
workerRegistryService := worker_registry_core.NewWorkerRegistryService(worker_repo.NewRepo())
|
||||
jobService := job_core.NewJobService(job_repo.NewRepo())
|
||||
// jobSchedulerService := job_scheduler_core.NewJobSchedulerService(nil, nil)
|
||||
|
||||
workerDaemon := worker_core.NewWorkerDaemon(
|
||||
worker_client.NewClient(workerRegistryService, jobService),
|
||||
worker_executor.NewExecutor(),
|
||||
&worker_ports.Config{
|
||||
Longitude: 1.0,
|
||||
Latitude: 1.0,
|
||||
GatewayUrl: "don't care",
|
||||
},
|
||||
)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
workerDone := make(chan struct{})
|
||||
// schedulerDone := make(chan struct{})
|
||||
|
||||
go workerDaemon.Start(ctx, workerDone)
|
||||
// go jobSchedulerService.Schedule(ctx, schedulerDone)
|
||||
|
||||
jobService.CreateJob(
|
||||
context.Background(),
|
||||
job_ports.CreateJobParams{
|
||||
Name: "A cool job",
|
||||
ImageName: "A cool image",
|
||||
ConsumerLongitude: 1.0,
|
||||
ConsumerLatitude: 1.0,
|
||||
EnvironmentVariables: make(map[string]string),
|
||||
},
|
||||
"Gandalf",
|
||||
)
|
||||
|
||||
for {
|
||||
jobs, err := jobService.GetJobsForConsumer(context.Background(), "Gandalf")
|
||||
if err != nil {
|
||||
fmt.Printf("Encountered error while polling jobs for Gandalf: %v\n", err)
|
||||
break
|
||||
}
|
||||
|
||||
if len(jobs) != 1 {
|
||||
fmt.Println("There should exactly be one job from Gandalf")
|
||||
break
|
||||
}
|
||||
|
||||
job := jobs[0]
|
||||
if job.Status == job_ports.FINISHED || job.Status == job_ports.FAILED {
|
||||
break
|
||||
}
|
||||
|
||||
fmt.Println("Waiting for Job to be done.")
|
||||
time.Sleep(3 * time.Second)
|
||||
|
||||
}
|
||||
|
||||
cancel()
|
||||
|
||||
<-workerDone
|
||||
// <-schedulerDone
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
module gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/cli/gcls-modulith
|
||||
|
||||
go 1.23.2
|
||||
|
||||
replace gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/services/gcls-worker-daemon => ../../services/gcls-worker-daemon
|
||||
|
||||
replace gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/services/job => ../../services/job
|
||||
|
||||
replace gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/services/worker-registry => ../../services/worker-registry
|
||||
|
||||
replace gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/services/job-scheduler => ../../services/job-scheduler
|
||||
|
||||
require (
|
||||
gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/services/gcls-worker-daemon v0.0.0-00010101000000-000000000000
|
||||
gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/services/job v0.0.0-00010101000000-000000000000
|
||||
gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/services/worker-registry v0.0.0-00010101000000-000000000000
|
||||
)
|
||||
|
||||
require github.com/google/uuid v1.6.0 // indirect
|
|
@ -0,0 +1,2 @@
|
|||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
@ -0,0 +1,7 @@
|
|||
package main
|
||||
|
||||
import "gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/cli/gcls-modulith/cmd"
|
||||
|
||||
func main() {
|
||||
cmd.Execute()
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
package client_cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/services/gcls-worker-daemon/ports"
|
||||
job_ports "gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/services/job/ports"
|
||||
worker_registry_ports "gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/services/worker-registry/ports"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
WorkerRegistryService worker_registry_ports.Api
|
||||
JobService job_ports.Api
|
||||
}
|
||||
|
||||
var _ ports.Client = (*Client)(nil)
|
||||
|
||||
func NewClient(workerRegistryService worker_registry_ports.Api, jobService job_ports.Api) *Client {
|
||||
return &Client{WorkerRegistryService: workerRegistryService, JobService: jobService}
|
||||
}
|
||||
|
||||
func (c *Client) Register(worker ports.Worker) error {
|
||||
return c.WorkerRegistryService.RegisterWorker(
|
||||
worker_registry_ports.Worker{
|
||||
Id: worker.Id,
|
||||
Status: worker_registry_ports.WorkerStatus(worker.Status),
|
||||
Location: worker_registry_ports.Location{
|
||||
Latitude: worker.Location.Latitude,
|
||||
Longitude: worker.Location.Longitude,
|
||||
},
|
||||
},
|
||||
context.Background())
|
||||
}
|
||||
|
||||
func (c *Client) Unregister(id string) error {
|
||||
return c.WorkerRegistryService.UnregisterWorker(id, context.Background())
|
||||
}
|
||||
|
||||
func (c *Client) GetJobs(id string) ([]ports.Job, error) {
|
||||
jobs, err := c.JobService.GetJobsForWorker(context.Background(), id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
workerJobs := make([]ports.Job, len(jobs))
|
||||
for i, job := range jobs {
|
||||
workerJobs[i] = ports.Job{
|
||||
Id: job.Id,
|
||||
ImageName: job.ImageName,
|
||||
EnvironmentVariables: job.EnvironmentVariables,
|
||||
Status: ports.JobStatus(job.Status),
|
||||
StartedAt: job.StartedAt,
|
||||
FinishedAt: job.FinishedAt,
|
||||
}
|
||||
}
|
||||
|
||||
return workerJobs, nil
|
||||
}
|
||||
|
||||
func (c *Client) UpdateStatus(id string, status ports.WorkerStatus) error {
|
||||
return c.WorkerRegistryService.UpdateWorker(id, worker_registry_ports.WorkerStatus(status), context.Background())
|
||||
}
|
||||
|
||||
func (c *Client) UpdateJob(id string, stdout string, startedAt int64, finishedAt int64, status ports.JobStatus) error {
|
||||
return c.JobService.UpdateJobForWorker(
|
||||
context.Background(),
|
||||
id,
|
||||
job_ports.UpdateJobForWorkerParams{
|
||||
StandardOutput: stdout,
|
||||
StartedAt: startedAt,
|
||||
FinishedAt: finishedAt,
|
||||
Status: job_ports.JobStatus(status),
|
||||
},
|
||||
)
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package executor_cli
|
||||
|
||||
import "gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/services/gcls-worker-daemon/ports"
|
||||
|
||||
type Executor struct{}
|
||||
|
||||
var _ ports.Executor = (*Executor)(nil)
|
||||
|
||||
func NewExecutor() *Executor {
|
||||
return &Executor{}
|
||||
}
|
||||
|
||||
func (c *Executor) Execute(job ports.Job) (ports.ExecutionResult, error) {
|
||||
return ports.ExecutionResult{
|
||||
Status: ports.SUCCESS,
|
||||
StartedAt: 0,
|
||||
FinishedAt: 0,
|
||||
Stdout: "Hello, World!",
|
||||
}, nil
|
||||
}
|
|
@ -0,0 +1,190 @@
|
|||
# Green Compute Load Shifting (GCLS)
|
||||
|
||||
## Use Cases
|
||||
|
||||
![use cases](use-cases.png)
|
||||
|
||||
## Building Blocks
|
||||
|
||||
![building blocks](building-blocks.png)
|
||||
|
||||
| Building Block | Type | Purpose | Owner |
|
||||
| ------ | --- | -------------- | --- |
|
||||
| GCLS Worker Daemon | CLI | Provide computing capacity to platform; Execution of jobs | Team 1 |
|
||||
| Worker Gateway | Microservice | Provides unified API for the GCLS Worker Daemon | Team 1 |
|
||||
| Worker Registry | Microservice | Holds Status of Worker Daemons | Team 1 |
|
||||
| GCLS Consumer CLI | CLI | Place compute jobs; Receive Job Status/Results | Team 2 |
|
||||
| Consumer Gateway | Microservice | Provides unified API for the GCLS Consumer CLI | Team 2 |
|
||||
| Job | Microservice | Manages the status of Jobs | Team 3 |
|
||||
| User Management | Microservice | Manages Users | Team 3 |
|
||||
| Job Scheduler | Microservice | Schedules Jobs | Team 4 |
|
||||
| Carbon Intensity Provider | Microservice | Provides Carbon Intensity | Team 4 |
|
||||
|
||||
## Approved Packages
|
||||
|
||||
- [google/uuid](https://pkg.go.dev/github.com/google/uuid)
|
||||
- [opentelemetry](https://pkg.go.dev/go.opentelemetry.io/otel)
|
||||
|
||||
## Runtime Scenarios
|
||||
|
||||
### Run Compute Job
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant worker as GCLS Worker Daemon
|
||||
participant gateway as Worker Gateway
|
||||
participant registry as Worker Registry
|
||||
participant job as Job
|
||||
|
||||
loop every 5s
|
||||
worker->>gateway: UpdateStatus(worker)
|
||||
activate gateway
|
||||
gateway->>registry: UpdateStatus(worker)
|
||||
activate registry
|
||||
registry-->>gateway: 200 OK
|
||||
deactivate registry
|
||||
gateway-->>worker: 200 OK
|
||||
deactivate gateway
|
||||
end
|
||||
|
||||
loop
|
||||
worker->>gateway: GetJob(worker)
|
||||
activate gateway
|
||||
gateway->>job: GetJob(worker)
|
||||
activate job
|
||||
job-->>gateway: Job
|
||||
deactivate job
|
||||
gateway-->>worker: Job
|
||||
deactivate gateway
|
||||
|
||||
worker->>worker: SetStatus("EXHAUSTED")
|
||||
worker->>worker: Execute(job)
|
||||
|
||||
worker->>gateway: Update(job)
|
||||
activate gateway
|
||||
gateway->>job: Update(job)
|
||||
activate job
|
||||
job-->>gateway: 200 OK
|
||||
deactivate job
|
||||
gateway-->>worker: 200 OK
|
||||
deactivate gateway
|
||||
|
||||
worker->>worker: SetStatus("REQUIRES_WORK")
|
||||
end
|
||||
```
|
||||
|
||||
### Create Compute Job
|
||||
|
||||
```mermaid create_job
|
||||
sequenceDiagram
|
||||
actor ComputeConsumer as Compute Consumer
|
||||
|
||||
ComputeConsumer ->> GCLS_CLI : Job anlegen mit Job-Definition
|
||||
|
||||
note right of GCLS_CLI: Annahme: Der User ist bereits registriert
|
||||
|
||||
GCLS_CLI ->> GCLS_CLI : JWT aus Speicher lesen
|
||||
|
||||
alt Gültiger JWT vorhanden
|
||||
GCLS_CLI ->> ConsumerGateway : POST /jobs (+ JWT)
|
||||
ConsumerGateway ->> ConsumerGateway : Validiere JWT
|
||||
alt JWT gültig
|
||||
ConsumerGateway ->> Job : POST /jobs (+ JWT)
|
||||
|
||||
alt Job und JWT gültig
|
||||
Job -->> ConsumerGateway : Job gültig und angelegt
|
||||
ConsumerGateway -->> GCLS_CLI : Job gültig und angelegt
|
||||
GCLS_CLI -->> ComputeConsumer : Job erfolgreich angelegt
|
||||
else Job ungültig
|
||||
Job -->> ConsumerGateway : Job ungültig
|
||||
ConsumerGateway -->> GCLS_CLI : Job ungültig
|
||||
GCLS_CLI -->> ComputeConsumer : Fehlernachricht "ungültiger Job"
|
||||
else JWT ungültig
|
||||
Job -->> ConsumerGateway : JWT ungültig
|
||||
ConsumerGateway -->> GCLS_CLI : JWT ungültig
|
||||
GCLS_CLI -->> ComputeConsumer : Fehlernachricht "nicht authentifiziert"
|
||||
end
|
||||
else JWT ungültig
|
||||
ConsumerGateway -->> GCLS_CLI : JWT ungültig
|
||||
GCLS_CLI -->> ComputeConsumer : Fehlernachricht "nicht authentifiziert"
|
||||
end
|
||||
else JWT nicht vorhanden oder abgelaufen
|
||||
GCLS_CLI -->> ComputeConsumer : Fehlernachricht "nicht authentifiziert"
|
||||
end
|
||||
```
|
||||
|
||||
### Login
|
||||
|
||||
```mermaid login
|
||||
sequenceDiagram
|
||||
actor ComputeConsumer as Compute Consumer
|
||||
ComputeConsumer ->> GCLS_CLI: Anmelden mit Nutzername + Passwort
|
||||
GCLS_CLI ->> ConsumerGateway: POST /login
|
||||
|
||||
ConsumerGateway ->> UserManagement: POST /login
|
||||
|
||||
alt Zugangsdaten gültig und Anfrage authentifiziert
|
||||
UserManagement -->> ConsumerGateway: JWT
|
||||
ConsumerGateway -->> GCLS_CLI: JWT
|
||||
GCLS_CLI ->> GCLS_CLI: JWT sicher speichern
|
||||
GCLS_CLI -->> ComputeConsumer: Erfolgreich angemeldet
|
||||
else Zugangsdaten ungültig
|
||||
UserManagement -->> ConsumerGateway: ungültige Zugangsdaten
|
||||
ConsumerGateway -->> GCLS_CLI: ungültige Zugangsdaten
|
||||
GCLS_CLI -->> ComputeConsumer: Fehlernachricht "ungültige Zugangsdaten"
|
||||
else Anfrage (über basic auth) nicht authentifiziert
|
||||
UserManagement -->> ConsumerGateway: Anfrage über basic auth nicht authentifiziert
|
||||
ConsumerGateway -->> GCLS_CLI: interner Fehler
|
||||
GCLS_CLI -->> ComputeConsumer: Fehlernachricht "interner Fehler"
|
||||
end
|
||||
```
|
||||
|
||||
### Get Job Statistics
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
actor Consumer
|
||||
note right of Consumer: Annahme: Der User ist bereits eingeloggt
|
||||
note right of GCLS_CLI: Annahme: Dauerhafte Netzwerk- und Endpoint-Erreichbarkeit
|
||||
Consumer ->> GCLS_CLI: Request Job statistics
|
||||
alt Gültiger JWT vorhanden
|
||||
GCLS_CLI ->> Consumer-Gateway : POST /jobs (+ JWT)
|
||||
Consumer-Gateway ->> Consumer-Gateway : Validiere JWT
|
||||
alt JWT gültig
|
||||
Consumer-Gateway ->> Job : POST /jobs (+ JWT)
|
||||
|
||||
alt Job und JWT gültig
|
||||
Job -->> Job: Retrieve statistics from database
|
||||
alt Database success
|
||||
Job -->> Consumer-Gateway : Return job statistics
|
||||
Consumer-Gateway -->> GCLS_CLI : Return job statistics
|
||||
GCLS_CLI -->> Consumer : Return job statistics
|
||||
else Database error
|
||||
Job -->> Consumer-Gateway : Retrieving failed
|
||||
Consumer-Gateway -->> GCLS_CLI : Retrieving failed
|
||||
GCLS_CLI -->> Consumer : Fehlernachricht "Datenbankfehler"
|
||||
end
|
||||
else JWT ungültig
|
||||
Job -->> Consumer-Gateway : JWT ungültig
|
||||
Consumer-Gateway -->> GCLS_CLI : JWT ungültig
|
||||
GCLS_CLI -->> Consumer : Fehlernachricht "nicht authentifiziert"
|
||||
end
|
||||
else JWT ungültig vom Consumer-Gateway
|
||||
Consumer-Gateway -->> GCLS_CLI : JWT ungültig
|
||||
GCLS_CLI -->> Consumer : Fehlernachricht "nicht authentifiziert"
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Schedule Job
|
||||
|
||||
![jobs-worker-cip](jobs-worker-cip.jpeg)
|
||||
|
||||
## Cross Cutting Concerns
|
||||
|
||||
| Aspect | Owner |
|
||||
| --- | --- |
|
||||
| Observability | Team 1 |
|
||||
| DevOps | Team 2 |
|
||||
| Security | Team 3 |
|
||||
| Communication Resilience | Team 4 |
|
Binary file not shown.
After Width: | Height: | Size: 91 KiB |
|
@ -0,0 +1,334 @@
|
|||
<mxfile host="Electron" agent="Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/24.7.8 Chrome/128.0.6613.36 Electron/32.0.1 Safari/537.36" version="24.7.8" pages="3">
|
||||
<diagram name="Overview" id="9SfsONUl5Bj8uXdVnY35">
|
||||
<mxGraphModel dx="4634" dy="1196" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1100" pageHeight="850" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0" />
|
||||
<mxCell id="1" parent="0" />
|
||||
<mxCell id="bkjyaAWGgHkUM0_QegR--12" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;sketch=1;curveFitting=1;jiggle=2;verticalAlign=bottom;" parent="1" vertex="1">
|
||||
<mxGeometry x="79.99999999999997" y="110" width="120" height="120" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="bkjyaAWGgHkUM0_QegR--1" value="" style="html=1;verticalLabelPosition=bottom;align=center;labelBackgroundColor=#ffffff;verticalAlign=top;strokeWidth=2;strokeColor=#0080F0;shadow=0;dashed=0;shape=mxgraph.ios7.icons.cloud;" parent="1" vertex="1">
|
||||
<mxGeometry x="236.22" y="70" width="530" height="170" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="bkjyaAWGgHkUM0_QegR--8" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="bkjyaAWGgHkUM0_QegR--3" target="bkjyaAWGgHkUM0_QegR--7" edge="1">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="bkjyaAWGgHkUM0_QegR--17" value="get current carbon intensity" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="bkjyaAWGgHkUM0_QegR--8" vertex="1" connectable="0">
|
||||
<mxGeometry x="-0.1849" y="-2" relative="1" as="geometry">
|
||||
<mxPoint x="4" y="8" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="bkjyaAWGgHkUM0_QegR--3" value="Green Compute Load Shifting (GCLS) Platform" style="rounded=0;whiteSpace=wrap;html=1;sketch=1;curveFitting=1;jiggle=2;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" vertex="1">
|
||||
<mxGeometry x="366.22" y="150" width="260" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="bkjyaAWGgHkUM0_QegR--4" value="Electricity Maps" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;sketch=1;curveFitting=1;jiggle=2;verticalAlign=bottom;" parent="1" vertex="1">
|
||||
<mxGeometry x="816.22" y="120" width="120" height="120" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="bkjyaAWGgHkUM0_QegR--7" value="" style="shape=image;verticalLabelPosition=bottom;labelBackgroundColor=default;verticalAlign=top;aspect=fixed;imageAspect=0;image=data:image/png,iVBORw0KGgoAAAANSUhEUgAAAOEAAADhCAMAAAAJbSJIAAAAaVBMVEX///8AAADx8fG8vLxDQ0OFhYX6+vrW1tYjIyP29vaBgYFVVVXl5eWXl5dRUVHCwsIWFhYaGhqWlpYODg7Ozs7r6+szMzNvb2+np6e2traOjo6fn5+tra1fX187Ozvg4OArKytpaWl3d3cBzJlsAAAD0klEQVR4nO3diWKiMBAGYNN6Ua1HrVp72fb9H3K36wGEJJAwOMPs/z3ANL9WJAMjgwEAAAAAAAAAAAAAAAAAQIzJaDgdjibq6lxkM3MyGxPVyUTUuZqb3FxRndyqUHHaos66UGfdos60UGfVos7Vgyl6UFPnamzKUj+K0urk7q2Ky8Q6S2F1ck9WxbfEOo9WnUfmOrmRVXGkpE5O/3uo/3Mo7RhIfyy1vn/S/+2p6pQ/iATfh+UXbZP+kpVf/BZ1NsRvobzzSfrz0kG2ONdruye41FkQ7S1a1imStq+j3h8CAAAAAAAAQLztlnsF3frt1Xyqzrj77SCRXNcUan9qkuntIb2c24D33AvpysQoT5gZ7QmH2hMWLj7pTHgwyhMejfKE45X2hDOjPOGrUZ7QvoFCXcKt0Z7wS3vCDzugtoRvlYDKEj5XA+pKeOcIqCvhp/aEe1dATQnfnQEVJZy4A+pJmHkC6kk41J7QvulcXUL7DnF1Cef+gDoSjtfaE84CAVUktAdR1CW02xaWQ3ZXhztBjUrbIt6m/TBMlyptixTP3CkCvikCmm/uGH6OtkWK3S3XHDXf4NtQxAqMNPHO4zvbFim8h9Mx8zy+s22RwHug4Z7H/yEK+OT9C8zz+J62RbQPovU0EDezeTQ0/O8N8zy+/eeT+T9fzPP4C0MjcGbOO4/vb1vEifkbN53HD7QtoixC6+Gcxw+1LaIEv8c55/FXhsaRaD2NNZ2jr14HTfNesx62efxw26K5n9r1MM3ju66DpmiwZeKZxydoW5w06c+wzONTnW837FwwzOPvaAKm/vjNDdAcSAV3ZmgONG1+DK57y9BlioakD5rUdrGzmt2x7BZwI+Ezc3/boj+CCYMbir4IJVyTnH5xCyUMbyj6IpDwwL02Gv6E7VsRMngTDrlXRsWbkG6LwMyXUM+8pSdhXduiR9wJ99zLIuRM+Mm9KkrOhNJvK4niSij5hot4joSC2xYpqgn910H7qZLwi3tF1CoJpbctolH9rLRcVsJX7vXQKyeccS+nA6WEKxVtC0spoY62haWYUEnbwlJISHHpVqA8oZq2hSVPqKZtYbkm1NO2sFwSvnAvpDPni4ya2ha2fwFveoP6rW13fw+jqtoWVdp/NBEAAAAAAABAOGnPLeSdx29Sh+gZlkzz+L2pk4ubx/crziusW9RhnsfvT50rac9iZp7HD5D7XG79z1aX9lx71nn8ILnvof7PobRjIOM8/q3qsM3jx9Vp8dJzzeP3sE5Bg3n8uDot5+hZ5vF7WgcAAAAAAAAAAAAAAAAA/i9/AOyoKhG6kvueAAAAAElFTkSuQmCC;" parent="1" vertex="1">
|
||||
<mxGeometry x="836.22" y="140" width="80" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="bkjyaAWGgHkUM0_QegR--10" value="" style="shape=image;verticalLabelPosition=bottom;labelBackgroundColor=default;verticalAlign=top;aspect=fixed;imageAspect=0;image=https://pages.okta.com/rs/855-QAH-699/images/email-main-template_auth0-by-okta-logo_black_279x127_3x.png;" parent="1" vertex="1">
|
||||
<mxGeometry x="85.08" y="150" width="109.84" height="50" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="bkjyaAWGgHkUM0_QegR--14" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.997;entryY=0.582;entryDx=0;entryDy=0;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryPerimeter=0;" parent="1" source="bkjyaAWGgHkUM0_QegR--3" target="bkjyaAWGgHkUM0_QegR--12" edge="1">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="bkjyaAWGgHkUM0_QegR--16" value="authentication" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="bkjyaAWGgHkUM0_QegR--14" vertex="1" connectable="0">
|
||||
<mxGeometry x="-0.3806" y="1" relative="1" as="geometry">
|
||||
<mxPoint x="-24" y="9" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="bkjyaAWGgHkUM0_QegR--18" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.25;entryY=1;entryDx=0;entryDy=0;exitX=0.5;exitY=0;exitDx=0;exitDy=0;" parent="1" source="bkjyaAWGgHkUM0_QegR--15" target="bkjyaAWGgHkUM0_QegR--3" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="261.22" y="270" />
|
||||
<mxPoint x="431.22" y="270" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="bkjyaAWGgHkUM0_QegR--19" value="Get Jobs&nbsp;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="bkjyaAWGgHkUM0_QegR--18" vertex="1" connectable="0">
|
||||
<mxGeometry x="-0.241" y="2" relative="1" as="geometry">
|
||||
<mxPoint x="70" y="-8" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="bkjyaAWGgHkUM0_QegR--37" value="" style="group" parent="1" vertex="1" connectable="0">
|
||||
<mxGeometry x="216.22" y="290" width="90" height="79" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="bkjyaAWGgHkUM0_QegR--15" value="GCLS Worker" style="rounded=0;whiteSpace=wrap;html=1;sketch=1;curveFitting=1;jiggle=2;fillColor=#d5e8d4;strokeColor=#82b366;verticalAlign=top;" parent="bkjyaAWGgHkUM0_QegR--37" vertex="1">
|
||||
<mxGeometry width="90" height="79" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="bkjyaAWGgHkUM0_QegR--2" value="" style="image;sketch=0;aspect=fixed;html=1;points=[];align=center;fontSize=12;image=img/lib/mscae/Docker.svg;" parent="bkjyaAWGgHkUM0_QegR--37" vertex="1">
|
||||
<mxGeometry x="20" y="30" width="50" height="41" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="bkjyaAWGgHkUM0_QegR--38" value="" style="group" parent="1" vertex="1" connectable="0">
|
||||
<mxGeometry x="316.22" y="290" width="90" height="79" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="bkjyaAWGgHkUM0_QegR--39" value="GCLS Worker" style="rounded=0;whiteSpace=wrap;html=1;sketch=1;curveFitting=1;jiggle=2;fillColor=#d5e8d4;strokeColor=#82b366;verticalAlign=top;" parent="bkjyaAWGgHkUM0_QegR--38" vertex="1">
|
||||
<mxGeometry width="90" height="79" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="bkjyaAWGgHkUM0_QegR--40" value="" style="image;sketch=0;aspect=fixed;html=1;points=[];align=center;fontSize=12;image=img/lib/mscae/Docker.svg;" parent="bkjyaAWGgHkUM0_QegR--38" vertex="1">
|
||||
<mxGeometry x="20" y="30" width="50" height="41" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="bkjyaAWGgHkUM0_QegR--41" value="" style="group" parent="1" vertex="1" connectable="0">
|
||||
<mxGeometry x="416.22" y="290" width="90" height="79" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="bkjyaAWGgHkUM0_QegR--42" value="GCLS Worker" style="rounded=0;whiteSpace=wrap;html=1;sketch=1;curveFitting=1;jiggle=2;fillColor=#d5e8d4;strokeColor=#82b366;verticalAlign=top;" parent="bkjyaAWGgHkUM0_QegR--41" vertex="1">
|
||||
<mxGeometry width="90" height="79" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="bkjyaAWGgHkUM0_QegR--43" value="" style="image;sketch=0;aspect=fixed;html=1;points=[];align=center;fontSize=12;image=img/lib/mscae/Docker.svg;" parent="bkjyaAWGgHkUM0_QegR--41" vertex="1">
|
||||
<mxGeometry x="20" y="30" width="50" height="41" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="bkjyaAWGgHkUM0_QegR--44" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.25;entryY=1;entryDx=0;entryDy=0;" parent="1" source="bkjyaAWGgHkUM0_QegR--39" target="bkjyaAWGgHkUM0_QegR--3" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="361.22" y="270" />
|
||||
<mxPoint x="431.22" y="270" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="bkjyaAWGgHkUM0_QegR--45" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.25;entryY=1;entryDx=0;entryDy=0;" parent="1" source="bkjyaAWGgHkUM0_QegR--42" target="bkjyaAWGgHkUM0_QegR--3" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="461.22" y="270" />
|
||||
<mxPoint x="431.22" y="270" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="bkjyaAWGgHkUM0_QegR--46" value="" style="group" parent="1" vertex="1" connectable="0">
|
||||
<mxGeometry x="606.22" y="290" width="90" height="79" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="bkjyaAWGgHkUM0_QegR--47" value="GCLS CLI" style="rounded=0;whiteSpace=wrap;html=1;sketch=1;curveFitting=1;jiggle=2;fillColor=#d5e8d4;strokeColor=#82b366;verticalAlign=top;" parent="bkjyaAWGgHkUM0_QegR--46" vertex="1">
|
||||
<mxGeometry width="90" height="79" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="bkjyaAWGgHkUM0_QegR--48" value="" style="image;sketch=0;aspect=fixed;html=1;points=[];align=center;fontSize=12;image=img/lib/mscae/Docker.svg;" parent="bkjyaAWGgHkUM0_QegR--46" vertex="1">
|
||||
<mxGeometry x="20" y="30" width="50" height="41" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="bkjyaAWGgHkUM0_QegR--49" value="<font style="font-size: 10px;">Compute Provider</font>" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="246.22" y="390" width="30" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="bkjyaAWGgHkUM0_QegR--56" value="<font style="font-size: 10px;">Compute Provider</font>" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="346.22" y="390" width="30" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="bkjyaAWGgHkUM0_QegR--57" value="<font style="font-size: 10px;">Compute Provider</font>" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="446.22" y="390" width="30" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="bkjyaAWGgHkUM0_QegR--58" value="<font style="font-size: 10px;">Compute Consumer</font>" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="636.22" y="390" width="30" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="bkjyaAWGgHkUM0_QegR--59" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.25;entryY=1;entryDx=0;entryDy=0;" parent="1" source="bkjyaAWGgHkUM0_QegR--47" target="bkjyaAWGgHkUM0_QegR--3" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="651.22" y="270" />
|
||||
<mxPoint x="431.22" y="270" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
<diagram id="bpHMyEXNbauoO1gqHrBm" name="Use Cases">
|
||||
<mxGraphModel dx="1184" dy="678" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0" />
|
||||
<mxCell id="1" parent="0" />
|
||||
<mxCell id="Tsc4UfA5dBkjLhyos3iL-1" value="Green Compute Load Shifter" style="swimlane;whiteSpace=wrap;html=1;startSize=23;" parent="1" vertex="1">
|
||||
<mxGeometry x="240" y="90" width="300" height="420" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="yRPADHfy2BcxKq2ry5dm-4" value="run compute job" style="ellipse;whiteSpace=wrap;html=1;" parent="Tsc4UfA5dBkjLhyos3iL-1" vertex="1">
|
||||
<mxGeometry x="10" y="330" width="120" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="yRPADHfy2BcxKq2ry5dm-5" value="create compute job" style="ellipse;whiteSpace=wrap;html=1;" parent="Tsc4UfA5dBkjLhyos3iL-1" vertex="1">
|
||||
<mxGeometry x="90" y="25" width="120" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="yRPADHfy2BcxKq2ry5dm-6" value="get compute job results" style="ellipse;whiteSpace=wrap;html=1;" parent="Tsc4UfA5dBkjLhyos3iL-1" vertex="1">
|
||||
<mxGeometry x="33" y="90" width="89" height="57" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="yRPADHfy2BcxKq2ry5dm-7" value="get job statistics" style="ellipse;whiteSpace=wrap;html=1;" parent="Tsc4UfA5dBkjLhyos3iL-1" vertex="1">
|
||||
<mxGeometry x="140" y="220" width="120" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="yRPADHfy2BcxKq2ry5dm-17" value="monitor system" style="ellipse;whiteSpace=wrap;html=1;" parent="Tsc4UfA5dBkjLhyos3iL-1" vertex="1">
|
||||
<mxGeometry x="175" y="300" width="120" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="yRPADHfy2BcxKq2ry5dm-19" value="d<span style="background-color: initial;">eploy new version</span>" style="ellipse;whiteSpace=wrap;html=1;" parent="Tsc4UfA5dBkjLhyos3iL-1" vertex="1">
|
||||
<mxGeometry x="150" y="100" width="120" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="Qs-BkcHP9t_8JN-s5nUO-1" value="login" style="ellipse;whiteSpace=wrap;html=1;" vertex="1" parent="Tsc4UfA5dBkjLhyos3iL-1">
|
||||
<mxGeometry x="2" y="192" width="120" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="OeV4-BEXwWvXPAKTQ9E--1" value="Compute Consumer" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="170" y="170" width="30" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="yRPADHfy2BcxKq2ry5dm-3" value="Compute Provider" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="170" y="360" width="30" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="yRPADHfy2BcxKq2ry5dm-8" value="Operations" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="580" y="270" width="30" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="yRPADHfy2BcxKq2ry5dm-11" value="" style="endArrow=none;html=1;rounded=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="1" target="yRPADHfy2BcxKq2ry5dm-5" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="210" y="170" as="sourcePoint" />
|
||||
<mxPoint x="270" y="200" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="yRPADHfy2BcxKq2ry5dm-12" value="" style="endArrow=none;html=1;rounded=0;" parent="1" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="210" y="200" as="sourcePoint" />
|
||||
<mxPoint x="270" y="200" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="yRPADHfy2BcxKq2ry5dm-13" value="" style="endArrow=none;html=1;rounded=0;" parent="1" target="yRPADHfy2BcxKq2ry5dm-4" edge="1" source="yRPADHfy2BcxKq2ry5dm-3">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="220" y="310" as="sourcePoint" />
|
||||
<mxPoint x="340" y="320" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="yRPADHfy2BcxKq2ry5dm-15" value="Developer" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="580" y="130" width="30" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="yRPADHfy2BcxKq2ry5dm-21" value="" style="endArrow=none;html=1;rounded=0;entryX=1;entryY=0;entryDx=0;entryDy=0;" parent="1" target="yRPADHfy2BcxKq2ry5dm-19" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="570" y="160" as="sourcePoint" />
|
||||
<mxPoint x="278" y="232" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="yRPADHfy2BcxKq2ry5dm-22" value="" style="endArrow=none;html=1;rounded=0;entryX=1.025;entryY=0.425;entryDx=0;entryDy=0;entryPerimeter=0;" parent="1" target="yRPADHfy2BcxKq2ry5dm-7" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="570" y="290" as="sourcePoint" />
|
||||
<mxPoint x="502" y="212" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="yRPADHfy2BcxKq2ry5dm-23" value="" style="endArrow=none;html=1;rounded=0;" parent="1" target="yRPADHfy2BcxKq2ry5dm-17" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="570" y="320" as="sourcePoint" />
|
||||
<mxPoint x="463" y="324" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="Qs-BkcHP9t_8JN-s5nUO-2" value="" style="endArrow=none;html=1;rounded=0;entryX=0.081;entryY=0.229;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" target="Qs-BkcHP9t_8JN-s5nUO-1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="210" y="230" as="sourcePoint" />
|
||||
<mxPoint x="278" y="222" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="Qs-BkcHP9t_8JN-s5nUO-3" value="" style="endArrow=none;html=1;rounded=0;exitX=0.028;exitY=0.671;exitDx=0;exitDy=0;exitPerimeter=0;" edge="1" parent="1" source="Qs-BkcHP9t_8JN-s5nUO-1" target="yRPADHfy2BcxKq2ry5dm-3">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="220" y="200" as="sourcePoint" />
|
||||
<mxPoint x="210" y="370" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="OxRlZlBOkbXn22t2wwnC-1" value="" style="endArrow=none;html=1;rounded=0;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="210" y="220" as="sourcePoint" />
|
||||
<mxPoint x="410" y="310" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
<diagram id="sQFYLQE7cxVQcqrETYkd" name="Building Blocks">
|
||||
<mxGraphModel dx="4634" dy="1196" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1100" pageHeight="850" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0" />
|
||||
<mxCell id="1" parent="0" />
|
||||
<mxCell id="LCirkKGBkMxsQzFvBwtC-2" value="Legend" style="rounded=0;whiteSpace=wrap;html=1;verticalAlign=top;align=left;" parent="1" vertex="1">
|
||||
<mxGeometry x="830" y="60" width="120" height="170" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="-GJQ50JBE78Y6x4tCiOK-1" value="<span style="color: rgb(0, 0, 0);">Green Compute Load Shifting (GCLS) Platform</span>" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#f5f5f5;fontColor=#333333;strokeColor=#666666;verticalAlign=top;" parent="1" vertex="1">
|
||||
<mxGeometry x="60" y="60" width="550" height="370" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="YncErUetCgZFby-mEPxv-1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="Y_95Ov9H_2BIZraajzK3-1" target="APDtbEWumEfnyccvbBP0-6" edge="1">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="YncErUetCgZFby-mEPxv-2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="Y_95Ov9H_2BIZraajzK3-1" target="w-Rq3PLQNwYjt5HaCK5d-1" edge="1">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="YncErUetCgZFby-mEPxv-3" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" parent="1" source="Y_95Ov9H_2BIZraajzK3-1" target="w-Rq3PLQNwYjt5HaCK5d-2" edge="1">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="Y_95Ov9H_2BIZraajzK3-1" value="Job Scheduler" style="shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fixedSize=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="280" y="100" width="120" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="sKb6c6YfNXrpK8ux16pL-1" value="Electricity Maps" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;sketch=1;curveFitting=1;jiggle=2;verticalAlign=bottom;" parent="1" vertex="1">
|
||||
<mxGeometry x="650" y="80" width="120" height="120" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="sKb6c6YfNXrpK8ux16pL-2" value="" style="shape=image;verticalLabelPosition=bottom;labelBackgroundColor=default;verticalAlign=top;aspect=fixed;imageAspect=0;image=data:image/png,iVBORw0KGgoAAAANSUhEUgAAAOEAAADhCAMAAAAJbSJIAAAAaVBMVEX///8AAADx8fG8vLxDQ0OFhYX6+vrW1tYjIyP29vaBgYFVVVXl5eWXl5dRUVHCwsIWFhYaGhqWlpYODg7Ozs7r6+szMzNvb2+np6e2traOjo6fn5+tra1fX187Ozvg4OArKytpaWl3d3cBzJlsAAAD0klEQVR4nO3diWKiMBAGYNN6Ua1HrVp72fb9H3K36wGEJJAwOMPs/z3ANL9WJAMjgwEAAAAAAAAAAAAAAAAAQIzJaDgdjibq6lxkM3MyGxPVyUTUuZqb3FxRndyqUHHaos66UGfdos60UGfVos7Vgyl6UFPnamzKUj+K0urk7q2Ky8Q6S2F1ck9WxbfEOo9WnUfmOrmRVXGkpE5O/3uo/3Mo7RhIfyy1vn/S/+2p6pQ/iATfh+UXbZP+kpVf/BZ1NsRvobzzSfrz0kG2ONdruye41FkQ7S1a1imStq+j3h8CAAAAAAAAQLztlnsF3frt1Xyqzrj77SCRXNcUan9qkuntIb2c24D33AvpysQoT5gZ7QmH2hMWLj7pTHgwyhMejfKE45X2hDOjPOGrUZ7QvoFCXcKt0Z7wS3vCDzugtoRvlYDKEj5XA+pKeOcIqCvhp/aEe1dATQnfnQEVJZy4A+pJmHkC6kk41J7QvulcXUL7DnF1Cef+gDoSjtfaE84CAVUktAdR1CW02xaWQ3ZXhztBjUrbIt6m/TBMlyptixTP3CkCvikCmm/uGH6OtkWK3S3XHDXf4NtQxAqMNPHO4zvbFim8h9Mx8zy+s22RwHug4Z7H/yEK+OT9C8zz+J62RbQPovU0EDezeTQ0/O8N8zy+/eeT+T9fzPP4C0MjcGbOO4/vb1vEifkbN53HD7QtoixC6+Gcxw+1LaIEv8c55/FXhsaRaD2NNZ2jr14HTfNesx62efxw26K5n9r1MM3ju66DpmiwZeKZxydoW5w06c+wzONTnW837FwwzOPvaAKm/vjNDdAcSAV3ZmgONG1+DK57y9BlioakD5rUdrGzmt2x7BZwI+Ezc3/boj+CCYMbir4IJVyTnH5xCyUMbyj6IpDwwL02Gv6E7VsRMngTDrlXRsWbkG6LwMyXUM+8pSdhXduiR9wJ99zLIuRM+Mm9KkrOhNJvK4niSij5hot4joSC2xYpqgn910H7qZLwi3tF1CoJpbctolH9rLRcVsJX7vXQKyeccS+nA6WEKxVtC0spoY62haWYUEnbwlJISHHpVqA8oZq2hSVPqKZtYbkm1NO2sFwSvnAvpDPni4ya2ha2fwFveoP6rW13fw+jqtoWVdp/NBEAAAAAAABAOGnPLeSdx29Sh+gZlkzz+L2pk4ubx/crziusW9RhnsfvT50rac9iZp7HD5D7XG79z1aX9lx71nn8ILnvof7PobRjIOM8/q3qsM3jx9Vp8dJzzeP3sE5Bg3n8uDot5+hZ5vF7WgcAAAAAAAAAAAAAAAAA/i9/AOyoKhG6kvueAAAAAElFTkSuQmCC;" parent="1" vertex="1">
|
||||
<mxGeometry x="670" y="100" width="80" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="YncErUetCgZFby-mEPxv-5" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="sKb6c6YfNXrpK8ux16pL-3" target="w-Rq3PLQNwYjt5HaCK5d-2" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="170" y="260" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="YncErUetCgZFby-mEPxv-6" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="sKb6c6YfNXrpK8ux16pL-3" target="9jAj6sHaBh6Y7vf6_xip-1" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="270" y="360" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="YncErUetCgZFby-mEPxv-9" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" parent="1" source="sKb6c6YfNXrpK8ux16pL-3" target="w-Rq3PLQNwYjt5HaCK5d-1" edge="1">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="sKb6c6YfNXrpK8ux16pL-3" value="Worker Gateway" style="shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fixedSize=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="90" y="320" width="120" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="APDtbEWumEfnyccvbBP0-1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" parent="1" source="sKb6c6YfNXrpK8ux16pL-4" target="sKb6c6YfNXrpK8ux16pL-3" edge="1">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="sKb6c6YfNXrpK8ux16pL-4" value="GCLS Worker Daemon" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="90" y="450" width="120" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="YncErUetCgZFby-mEPxv-4" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="yRverL1VW2yy343f3klw-1" target="w-Rq3PLQNwYjt5HaCK5d-2" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="530" y="260" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="YncErUetCgZFby-mEPxv-7" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="yRverL1VW2yy343f3klw-1" target="9jAj6sHaBh6Y7vf6_xip-1" edge="1">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="yRverL1VW2yy343f3klw-1" value="Consumer Gateway" style="shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fixedSize=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="470" y="320" width="120" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="APDtbEWumEfnyccvbBP0-2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="YJTWj9HRLNrw3p02Uit6-1" target="yRverL1VW2yy343f3klw-1" edge="1">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="YJTWj9HRLNrw3p02Uit6-1" value="GCLS Consumer CLI" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="470" y="450" width="120" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="APDtbEWumEfnyccvbBP0-7" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="APDtbEWumEfnyccvbBP0-6" target="sKb6c6YfNXrpK8ux16pL-1" edge="1">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="APDtbEWumEfnyccvbBP0-6" value="Carbon Intensity Provider" style="shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fixedSize=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="470" y="100" width="120" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="9jAj6sHaBh6Y7vf6_xip-1" value="User Management" style="shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fixedSize=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="280" y="320" width="120" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="w-Rq3PLQNwYjt5HaCK5d-1" value="Worker Registry" style="shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fixedSize=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="90" y="100" width="120" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="w-Rq3PLQNwYjt5HaCK5d-2" value="Job" style="shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fixedSize=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="280" y="220" width="120" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="OQUEwtuhqqt-KE0pjZ90-1" value="Microservice" style="shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fixedSize=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="860" y="72.5" width="75" height="50" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="OQUEwtuhqqt-KE0pjZ90-3" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="930" y="220" as="targetPoint" />
|
||||
<mxPoint x="860" y="220" as="sourcePoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="OQUEwtuhqqt-KE0pjZ90-4" value="Depends on" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="OQUEwtuhqqt-KE0pjZ90-3" vertex="1" connectable="0">
|
||||
<mxGeometry x="-0.2952" y="1" relative="1" as="geometry">
|
||||
<mxPoint x="5" y="-9" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="OQUEwtuhqqt-KE0pjZ90-5" value="Client Executable" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="855" y="140" width="80" height="45" as="geometry" />
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
|
@ -0,0 +1,39 @@
|
|||
package job
|
||||
|
||||
type Job struct {
|
||||
id string
|
||||
name string // descriptive name for job, helps consumers to identify job
|
||||
consumerId string // defined at job creation
|
||||
workerId string // defined when job assigned to worker
|
||||
imageName string // Name from DockerHub, e.g. "postgres" or "chainguard/jdk"
|
||||
environmentVariables map[string]string // e.g. {"POSTGRES_DB": "test_db", ...}
|
||||
status JobStatus
|
||||
standardOutput string
|
||||
|
||||
createdAt int64 // for priority -> first come first serve
|
||||
// Unix timestamps in milliseconds, needed to calculate estimated CO2 equivalent:
|
||||
startedAt int64
|
||||
finishedAt int64
|
||||
|
||||
// needed for calculating the diff between executing job at worker and consumer location
|
||||
ConsumerLongitude float64
|
||||
ConsumerLatitude float64
|
||||
Co2EquivalentEmissionConsumer float64 // emission of the consumer location (the current emission when job is scheduled)
|
||||
|
||||
WorkerLongitude float64
|
||||
WorkerLatitude float64
|
||||
Co2EquivalentEmissionWorker float64 // emission of the worker location (the current emission when job is scheduled)
|
||||
|
||||
// final co2 equivalent of the finished job
|
||||
estimatedCo2Equivalent float64
|
||||
}
|
||||
|
||||
type JobStatus string
|
||||
|
||||
const (
|
||||
CREATED JobStatus = "CREATED" // job created, but not assigned to worker yet
|
||||
PENDING JobStatus = "PENDING" // assigned to worker, but not running yet
|
||||
RUNNING JobStatus = "RUNNING"
|
||||
FINISHED JobStatus = "FINISHED"
|
||||
FAILED JobStatus = "FAILED"
|
||||
)
|
Binary file not shown.
After Width: | Height: | Size: 71 KiB |
Binary file not shown.
After Width: | Height: | Size: 141 KiB |
Binary file not shown.
After Width: | Height: | Size: 83 KiB |
|
@ -0,0 +1,21 @@
|
|||
# This file is maintained automatically by "terraform init".
|
||||
# Manual edits may be lost in future updates.
|
||||
|
||||
provider "registry.terraform.io/hashicorp/google" {
|
||||
version = "6.2.0"
|
||||
hashes = [
|
||||
"h1:7JIgzQKRW0AT6UliiYSjYUKxDr03baZpQmt5XVkrujs=",
|
||||
"zh:08a7dc0b53d2b63baab928e66086bf3e09107516af078ce011d2667456e64834",
|
||||
"zh:1cf9a1373e516844b43fdcea36e73f5a68f19ad07afcf6093788eb235c710163",
|
||||
"zh:2d4a7cb26c3f0d036d51db219a09013d3d779e44d584e0fc631df0f2cd5e5550",
|
||||
"zh:47e1fc68e455f99f1875deaed9aa5434a852e2a70a3cb5a5e9b5a2d8c25d7b74",
|
||||
"zh:78531a8624ddcd45277e1b465e773ac92001ea0e200e9dc1147ebeb24d56359e",
|
||||
"zh:a76751723c034d44764df22925178f78d8b4852e3e6ac6c5d86f51666c9e666c",
|
||||
"zh:a83a59a7e667cfffb0d501a501e9b3d2d4fcc83deb07a318c9690d537cbdc4b6",
|
||||
"zh:b16473b7e59e01690d8234a0044c304505688f5518b205e9ed06fc63ddc82977",
|
||||
"zh:b957648ad0383e17149bf3a02def81ebc6bd55ca0cffb6ec1c368a1b4f33c4fd",
|
||||
"zh:e2f3f4a27b41a20bdbb7a80fbcde1a4c36bbd1c83edb9256bc1724754f8d370f",
|
||||
"zh:ecfce738f85a81603aa51162d5237d6faaa2ffc0f0e52694f8b420ad761a8957",
|
||||
"zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c",
|
||||
]
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
# Infrastructure
|
||||
|
||||
This folder is reserved for defining the cloud infrastructure using terraform.
|
||||
|
||||
> **NOTE**
|
||||
> Run `gcloud auth application-default login` before running terraform to gain access to the project.
|
|
@ -0,0 +1,3 @@
|
|||
provider "google" {
|
||||
project = "cloud-infra-demo"
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
# Pkg-Folder
|
||||
|
||||
This folder is reserved for Go packages that can be used in multiple services. It is intended for the implementation of cross cutting concerns. Do not share any domain specific code via a package. Use API calls instead.
|
|
@ -0,0 +1,3 @@
|
|||
module even_odd
|
||||
|
||||
go 1.23.2
|
|
@ -0,0 +1,60 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
)
|
||||
|
||||
func SortArrayByParity(Input string) (map[string][]int, error) {
|
||||
|
||||
//array of type int for unmarshal
|
||||
|
||||
var nums []int
|
||||
|
||||
//unmarshal the JSON String from above
|
||||
|
||||
err := json.Unmarshal([]byte(Input), &nums)
|
||||
|
||||
if err != nil {
|
||||
|
||||
fmt.Println("error parsing JSON:", err)
|
||||
return nil, err
|
||||
}
|
||||
//map to store odd and even numbers seperate
|
||||
numMap := map[string][]int{
|
||||
|
||||
"odd": {},
|
||||
"even": {},
|
||||
}
|
||||
//sorting after odd and even
|
||||
for _, num := range nums {
|
||||
|
||||
if num%2 == 0 {
|
||||
numMap["even"] = append(numMap["even"], num)
|
||||
} else {
|
||||
|
||||
numMap["odd"] = append(numMap["odd"], num)
|
||||
}
|
||||
}
|
||||
//sorting the individual arrays
|
||||
sort.Ints(numMap["even"])
|
||||
sort.Ints(numMap["odd"])
|
||||
|
||||
return numMap, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
//example input
|
||||
jsonInput := `[2, 5, 9, 12, 36, 43]`
|
||||
|
||||
sortedMap, err := SortArrayByParity(jsonInput)
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("Error:", err)
|
||||
} else {
|
||||
fmt.Printf("Result: %v\n", sortedMap)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// case 1: Test the sort method with Input
|
||||
func TestSortArrayByParity(t *testing.T) {
|
||||
|
||||
jsonInput := `[4, 50, 22, 5, 17, 32]`
|
||||
|
||||
expected := map[string][]int{
|
||||
|
||||
"even": {4, 22, 32, 50},
|
||||
"odd": {5, 17},
|
||||
}
|
||||
result, err := SortArrayByParity(jsonInput)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(result, expected) {
|
||||
|
||||
t.Errorf("Test failed. Expected %v, got %v", expected, result)
|
||||
}
|
||||
|
||||
//case 2: empty array
|
||||
|
||||
jsonInput = `[]`
|
||||
|
||||
expected = map[string][]int{
|
||||
|
||||
"even": {},
|
||||
"odd": {},
|
||||
}
|
||||
|
||||
result, err = SortArrayByParity(jsonInput)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(result, expected) {
|
||||
|
||||
t.Errorf("Test failed. Expected %v, got %v", expected, result)
|
||||
}
|
||||
|
||||
//case 3: All odd numbers
|
||||
|
||||
jsonInput = `[1, 3, 5, 7, 9]`
|
||||
|
||||
expected = map[string][]int{
|
||||
|
||||
"even": {},
|
||||
"odd": {1, 3, 5, 7, 9},
|
||||
}
|
||||
result, err = SortArrayByParity(jsonInput)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(result, expected) {
|
||||
|
||||
t.Errorf("Test failed. Expected %v, got %v", expected, result)
|
||||
}
|
||||
|
||||
//case 4: All even numbers
|
||||
|
||||
jsonInput = `[2, 4, 6, 8, 10]`
|
||||
|
||||
expected = map[string][]int{
|
||||
|
||||
"even": {2, 4, 6, 8, 10},
|
||||
"odd": {},
|
||||
}
|
||||
result, err = SortArrayByParity(jsonInput)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(result, expected) {
|
||||
|
||||
t.Errorf("Test failed. Expected %v, got %v", expected, result)
|
||||
}
|
||||
|
||||
//case 5: invalid Format
|
||||
|
||||
jsonInput = `[1, 2, abc]`
|
||||
|
||||
_, err = SortArrayByParity(jsonInput)
|
||||
if err == nil {
|
||||
t.Errorf("expected error but got none")
|
||||
} else {
|
||||
var syntaxError *json.SyntaxError
|
||||
if !errors.As(err, &syntaxError) {
|
||||
t.Errorf("expected json.SyntaxError but got %T", err)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
module gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/pkg/learn-go/2024219
|
||||
|
||||
go 1.23.1
|
|
@ -0,0 +1 @@
|
|||
[12, 45, 23, 67, 89, 34, 78, 90, 11, 56, 32, 77, 54, 21, 88, 99, 17, 38, 65, 48, 22, 13, 72, 83, 61, 27]
|
|
@ -0,0 +1,60 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"sort"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
data, err := os.ReadFile("numbers.json")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to open file: %v", err)
|
||||
}
|
||||
|
||||
result, err := SeparateOddEven(string(data))
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to separate odd and even numbers: %v", err)
|
||||
}
|
||||
|
||||
PrintOddEvenMap(os.Stdout, result)
|
||||
}
|
||||
|
||||
// SeparateOddEven sorts and separates odd and even numbers from a JSON array input
|
||||
// assuming that file handling is done elsewehere and that we pass the input as a string.
|
||||
func SeparateOddEven(jsonInput string) (map[string][]int, error) {
|
||||
|
||||
var numbers []int
|
||||
err := json.Unmarshal([]byte(jsonInput), &numbers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make(map[string][]int)
|
||||
result["odd"] = []int{}
|
||||
result["even"] = []int{}
|
||||
|
||||
for _, num := range numbers {
|
||||
if num%2 == 0 {
|
||||
result["even"] = append(result["even"], num)
|
||||
} else {
|
||||
result["odd"] = append(result["odd"], num)
|
||||
}
|
||||
}
|
||||
sort.Ints(result["odd"])
|
||||
sort.Ints(result["even"])
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// additional method for printing the result of SeparateOddEven.
|
||||
// this prints to an io.Writer instead of the standard output
|
||||
// to enable easy testing of the function.
|
||||
func PrintOddEvenMap(w io.Writer, result map[string][]int) {
|
||||
fmt.Fprintf(w, "Odd numbers: %v\n", result["odd"])
|
||||
fmt.Fprintf(w, "Even numbers: %v\n", result["even"])
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestSeparateOddEven tests the SeparateOddEven function with various test cases.
|
||||
func TestSeparateOddEven(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
jsonInput string
|
||||
expected map[string][]int
|
||||
expectedError bool
|
||||
}{
|
||||
{
|
||||
name: "Valid input with mixed odd and even numbers",
|
||||
jsonInput: `[1, 4, 5, 8, 9, 2, 11, 14, 7]`,
|
||||
expected: map[string][]int{
|
||||
"even": {2, 4, 8, 14},
|
||||
"odd": {1, 5, 7, 9, 11},
|
||||
},
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
name: "All even numbers",
|
||||
jsonInput: `[2, 4, 6, 8, 10]`,
|
||||
expected: map[string][]int{
|
||||
"even": {2, 4, 6, 8, 10},
|
||||
"odd": {},
|
||||
},
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
name: "All odd numbers",
|
||||
jsonInput: `[1, 3, 5, 7, 9]`,
|
||||
expected: map[string][]int{
|
||||
"even": {},
|
||||
"odd": {1, 3, 5, 7, 9},
|
||||
},
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
name: "Empty input",
|
||||
jsonInput: `[]`,
|
||||
expected: map[string][]int{"even": {}, "odd": {}},
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid JSON input",
|
||||
jsonInput: `[1, 2, "abc"]`, // Invalid JSON
|
||||
expected: nil,
|
||||
expectedError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := SeparateOddEven(tt.jsonInput)
|
||||
if (err != nil) != tt.expectedError {
|
||||
t.Errorf("SeparateOddEven() error = %v, expectedError %v", err, tt.expectedError)
|
||||
return
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(result, tt.expected) {
|
||||
t.Errorf("SeparateOddEven() = %v, expected %v", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestPrintOddEvenMap tests the PrintOddEvenMap function by capturing the output to a buffer.
|
||||
func TestPrintOddEvenMap(t *testing.T) {
|
||||
tests := []struct {
|
||||
description string
|
||||
input map[string][]int
|
||||
expectedOutput string
|
||||
}{
|
||||
{
|
||||
description: "Mixed odd and even numbers",
|
||||
input: map[string][]int{
|
||||
"odd": {1, 3, 5},
|
||||
"even": {2, 4, 6},
|
||||
},
|
||||
expectedOutput: "Odd numbers: [1 3 5]\nEven numbers: [2 4 6]\n",
|
||||
},
|
||||
{
|
||||
description: "Only even numbers",
|
||||
input: map[string][]int{
|
||||
"odd": {},
|
||||
"even": {2, 4, 6},
|
||||
},
|
||||
expectedOutput: "Odd numbers: []\nEven numbers: [2 4 6]\n",
|
||||
},
|
||||
{
|
||||
description: "Only odd numbers",
|
||||
input: map[string][]int{
|
||||
"odd": {1, 3, 5},
|
||||
"even": {},
|
||||
},
|
||||
expectedOutput: "Odd numbers: [1 3 5]\nEven numbers: []\n",
|
||||
},
|
||||
{
|
||||
description: "Empty map",
|
||||
input: map[string][]int{
|
||||
"odd": {},
|
||||
"even": {},
|
||||
},
|
||||
expectedOutput: "Odd numbers: []\nEven numbers: []\n",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.description, func(t *testing.T) {
|
||||
var output bytes.Buffer
|
||||
PrintOddEvenMap(&output, tc.input)
|
||||
|
||||
if output.String() != tc.expectedOutput {
|
||||
t.Errorf("got %q, expected %q", output.String(), tc.expectedOutput)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var jsonDatStr = "[1,2,3,4,5,6,8,9,0,10,11]"
|
||||
|
||||
fmt.Print(getSortedIntegerSlices(jsonDatStr))
|
||||
}
|
||||
|
||||
func getSortedIntegerSlices(inputStr string) map[string][]int {
|
||||
var inputArray []int
|
||||
json.Unmarshal([]byte(inputStr), &inputArray)
|
||||
|
||||
var even []int
|
||||
var odd []int
|
||||
|
||||
for _, e := range inputArray {
|
||||
if e%2 == 0 {
|
||||
even = append(even, e)
|
||||
} else {
|
||||
odd = append(odd, e)
|
||||
}
|
||||
sort.Ints(even)
|
||||
sort.Ints(odd)
|
||||
}
|
||||
|
||||
return map[string][]int{
|
||||
"even": even,
|
||||
"odd": odd,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetSortedIntegerSlices(t *testing.T) {
|
||||
testCaseMap := map[string]struct {
|
||||
input string
|
||||
even []int
|
||||
odd []int
|
||||
}{
|
||||
"testA": {
|
||||
input: "[4,3,2,1]",
|
||||
even: []int{2, 4},
|
||||
odd: []int{1, 3},
|
||||
},
|
||||
"testB": {
|
||||
input: "[1,2,3,4,5,67]",
|
||||
even: []int{2, 4},
|
||||
odd: []int{1, 3, 5, 67},
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCaseMap {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
|
||||
result := getSortedIntegerSlices(testCase.input)
|
||||
|
||||
if !reflect.DeepEqual(result["even"], testCase.even) {
|
||||
t.Errorf("For input %v: expected even %v, got %v", testCase.input, testCase.even, result["even"])
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(result["odd"], testCase.odd) {
|
||||
t.Errorf("For input %v: expected odd %v, got %v", testCase.input, testCase.odd, result["odd"])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
module 2112107
|
||||
|
||||
go 1.19
|
|
@ -0,0 +1,3 @@
|
|||
module gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/pkg/learn-go/2121190
|
||||
|
||||
go 1.23.1
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"integers": [
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
10,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
5
|
||||
]
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/pkg/learn-go/2121190/odd_and_even"
|
||||
)
|
||||
|
||||
func main() {
|
||||
numbers, error := odd_and_even.GetArrayFromJSON("./integers.json")
|
||||
|
||||
if error != nil {
|
||||
fmt.Println(error)
|
||||
} else {
|
||||
numbersMap := odd_and_even.ConvertArrayToMapAndSort(numbers)
|
||||
fmt.Println(numbersMap)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
package odd_and_even
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"sort"
|
||||
)
|
||||
|
||||
type Integers struct {
|
||||
Integers []int `json:"integers"`
|
||||
}
|
||||
|
||||
func GetArrayFromJSON(filePath string) ([]int, error) {
|
||||
jsonFile, error := os.Open(filePath)
|
||||
|
||||
if error != nil {
|
||||
jsonFile.Close()
|
||||
return nil, errors.New("File cannot be opened.")
|
||||
} else {
|
||||
defer jsonFile.Close() // Close after this function returns
|
||||
|
||||
byteValue, _ := io.ReadAll(jsonFile)
|
||||
var integers Integers
|
||||
|
||||
json.Unmarshal(byteValue, &integers)
|
||||
|
||||
return integers.Integers, nil
|
||||
}
|
||||
}
|
||||
|
||||
func isEven(number int) bool {
|
||||
return number%2 == 0
|
||||
}
|
||||
|
||||
func appendAndSort(numbers []int, number int) []int {
|
||||
newNumbers := append(numbers, number)
|
||||
sort.Ints(newNumbers)
|
||||
return newNumbers
|
||||
}
|
||||
|
||||
func ConvertArrayToMapAndSort(numbers []int) map[string][]int {
|
||||
sortedNumbersMap := make(map[string][]int)
|
||||
sortedNumbersMap["odd"] = make([]int, 0)
|
||||
sortedNumbersMap["even"] = make([]int, 0)
|
||||
|
||||
for _, number := range numbers {
|
||||
if isEven(number) {
|
||||
sortedNumbersMap["even"] = appendAndSort(sortedNumbersMap["even"], number)
|
||||
} else {
|
||||
sortedNumbersMap["odd"] = appendAndSort(sortedNumbersMap["odd"], number)
|
||||
}
|
||||
}
|
||||
return sortedNumbersMap
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
package odd_and_even
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetArrayFromJSONValidPath(t *testing.T) {
|
||||
// GIVEN
|
||||
filePath := "../integers.json"
|
||||
|
||||
// WHEN
|
||||
numbers, error := GetArrayFromJSON(filePath)
|
||||
|
||||
// THEN
|
||||
expectedNumbers := []int{0, 1, 2, 3, 4, 10, 6, 7, 8, 9, 5}
|
||||
|
||||
if !reflect.DeepEqual(numbers, expectedNumbers) {
|
||||
t.Errorf("Numbers not as expected.\n Expected: %v\n Received: %v\n", expectedNumbers, numbers)
|
||||
}
|
||||
|
||||
if error != nil {
|
||||
t.Errorf("Expected no error to be thrown.\n Error: %v\n", error)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetArrayFromJSONEmptyPath(t *testing.T) {
|
||||
// GIVEN
|
||||
filePath := ""
|
||||
|
||||
// WHEN
|
||||
numbers, error := GetArrayFromJSON(filePath)
|
||||
|
||||
// THEN
|
||||
|
||||
if numbers != nil {
|
||||
t.Errorf("Numbers not as expected.\n Expected: %v\n Received: %v\n", nil, numbers)
|
||||
}
|
||||
|
||||
if error == nil {
|
||||
t.Errorf("Expected error to be thrown.\n")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertArrayToMapAndSortArrayAlreadySorted(t *testing.T) {
|
||||
// GIVEN
|
||||
numbers := []int{0, 1, 2, 3, 4, 5}
|
||||
|
||||
// WHEN
|
||||
numbersMap := ConvertArrayToMapAndSort(numbers)
|
||||
|
||||
// THEN
|
||||
expectedMap := map[string][]int{
|
||||
"even": []int{0, 2, 4},
|
||||
"odd": []int{1, 3, 5},
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(numbersMap, expectedMap) {
|
||||
t.Errorf("NumbersMap not as expected.\n Expected: %v\n Received: %v\n", expectedMap, numbersMap)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertArrayToMapAndSortArrayNotSorted(t *testing.T) {
|
||||
// GIVEN
|
||||
numbers := []int{9, 5, 7, 1, 3, 2}
|
||||
|
||||
// WHEN
|
||||
numbersMap := ConvertArrayToMapAndSort(numbers)
|
||||
|
||||
// THEN
|
||||
expectedMap := map[string][]int{
|
||||
"even": []int{2},
|
||||
"odd": []int{1, 3, 5, 7, 9},
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(numbersMap, expectedMap) {
|
||||
t.Errorf("NumbersMap not as expected.\n Expected: %v\n Received: %v\n", expectedMap, numbersMap)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertArrayToMapAndSortArrayEmpty(t *testing.T) {
|
||||
// GIVEN
|
||||
numbers := []int{}
|
||||
|
||||
// WHEN
|
||||
numbersMap := ConvertArrayToMapAndSort(numbers)
|
||||
|
||||
// THEN
|
||||
expectedMap := map[string][]int{
|
||||
"even": []int{},
|
||||
"odd": []int{},
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(numbersMap, expectedMap) {
|
||||
t.Errorf("NumbersMap not as expected.\n Expected: %v\n Received: %v\n", expectedMap, numbersMap)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
module oddEven
|
||||
|
||||
go 1.23.2
|
|
@ -0,0 +1,7 @@
|
|||
package main
|
||||
|
||||
import "oddEven/oddEven"
|
||||
|
||||
func main() {
|
||||
oddEven.OddEven()
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"numbers" : [4, 1, 3, 7, 6, 9, 0, 8, 5, 2]
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
package oddEven
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
)
|
||||
|
||||
type Numbers struct {
|
||||
Numbers []int `json:"numbers"`
|
||||
}
|
||||
|
||||
func OddEven() (map[string][]int, error) {
|
||||
filepath, err := filepath.Abs("./numbers.json")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
jsonFile, err := os.Open(filepath)
|
||||
defer jsonFile.Close()
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
byteArray, _ := io.ReadAll(jsonFile)
|
||||
|
||||
var numbersjson Numbers
|
||||
|
||||
json.Unmarshal(byteArray, &numbersjson)
|
||||
|
||||
var list []int
|
||||
list = numbersjson.Numbers
|
||||
|
||||
var odd []int
|
||||
var even []int
|
||||
|
||||
for i := 0; i < len(list); i++ {
|
||||
if list[i]%2 == 0 {
|
||||
even = append(even, list[i])
|
||||
} else {
|
||||
odd = append(odd, list[i])
|
||||
}
|
||||
}
|
||||
|
||||
slices.Sort(odd)
|
||||
slices.Sort(even)
|
||||
|
||||
result := map[string][]int{
|
||||
"odd": odd,
|
||||
"even": even,
|
||||
}
|
||||
|
||||
fmt.Println(result)
|
||||
|
||||
fmt.Println(result["odd"])
|
||||
|
||||
return result, nil
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package oddEven
|
||||
|
||||
import (
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEvenOdd(t *testing.T) {
|
||||
err := os.Chdir("..")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
is, err := OddEven()
|
||||
if err != nil {
|
||||
t.Log(err)
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
should := map[string][]int{
|
||||
"odd": []int{1, 3, 5, 7, 9},
|
||||
"even": []int{0, 2, 4, 6, 8},
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(is, should) {
|
||||
t.Logf("Expected : %v", should["odd"])
|
||||
|
||||
for k := range is {
|
||||
t.Log(k)
|
||||
t.Log(is[k])
|
||||
|
||||
}
|
||||
t.Logf("Was: %v", is["odd"])
|
||||
t.Fail()
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
module gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/pkg/learn-go/2122245
|
||||
|
||||
go 1.23.1
|
|
@ -0,0 +1,19 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/pkg/learn-go/2122245/parity"
|
||||
)
|
||||
|
||||
func main() {
|
||||
numbers := "[1, 5, 3, -8, 2, -3, 4, 5, -5, 0, 42]"
|
||||
seperatedNumbers, err := parity.SeperateEvenAndOddNumbers(numbers)
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Something went wrong: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println(seperatedNumbers)
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package parity
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"slices"
|
||||
)
|
||||
|
||||
func SeperateEvenAndOddNumbers(jsonString string) (map[string][]int, error) {
|
||||
var numbers []int
|
||||
err := json.Unmarshal([]byte(jsonString), &numbers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
seperatedNumbers := map[string][]int{
|
||||
"even": make([]int, 0),
|
||||
"odd": make([]int, 0),
|
||||
}
|
||||
|
||||
for _, number := range numbers {
|
||||
parity := "odd"
|
||||
if number%2 == 0 {
|
||||
parity = "even"
|
||||
}
|
||||
seperatedNumbers[parity] = append(seperatedNumbers[parity], number)
|
||||
}
|
||||
|
||||
slices.Sort(seperatedNumbers["odd"])
|
||||
slices.Sort(seperatedNumbers["even"])
|
||||
|
||||
return seperatedNumbers, nil
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
package parity
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParitySeperator(t *testing.T) {
|
||||
test_params := []struct {
|
||||
input string
|
||||
expected_output map[string][]int
|
||||
}{
|
||||
{"[9, 5, -5, 5, 1, 2]",
|
||||
map[string][]int{
|
||||
"even": {2},
|
||||
"odd": {-5, 1, 5, 5, 9},
|
||||
},
|
||||
},
|
||||
{"[4, 6, -2]",
|
||||
map[string][]int{
|
||||
"even": {-2, 4, 6},
|
||||
"odd": {},
|
||||
},
|
||||
},
|
||||
{"[9, -7, 1]",
|
||||
map[string][]int{
|
||||
"even": {},
|
||||
"odd": {-7, 1, 9},
|
||||
},
|
||||
},
|
||||
|
||||
{"[]",
|
||||
map[string][]int{
|
||||
"even": {},
|
||||
"odd": {},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test_param := range test_params {
|
||||
t.Run(fmt.Sprintf("Test with params %v", test_param), func(t *testing.T) {
|
||||
output, err := SeperateEvenAndOddNumbers(test_param.input)
|
||||
if err != nil {
|
||||
t.Fatalf("Error: '%v'", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(output, test_param.expected_output) {
|
||||
t.Fatalf("Expected '%v', but got '%v'", test_param.expected_output, output)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
module gitty.informatik.hs-mannheim.de/steger/cmg-ws202425/pkg/learn-go/2123801
|
||||
|
||||
go 1.23.1
|
|
@ -0,0 +1,43 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
)
|
||||
|
||||
func SortOddEven(jsonData string) (map[string][]int, error) {
|
||||
var numbers []int
|
||||
err := json.Unmarshal([]byte(jsonData), &numbers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := map[string][]int{
|
||||
"odd": {},
|
||||
"even": {},
|
||||
}
|
||||
|
||||
for _, num := range numbers {
|
||||
if num%2 == 0 {
|
||||
result["even"] = append(result["even"], num)
|
||||
} else {
|
||||
result["odd"] = append(result["odd"], num)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Ints(result["odd"])
|
||||
sort.Ints(result["even"])
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
jsonData := `[5, 8, 3, 10, 1, 12, 9]`
|
||||
result, err := SortOddEven(jsonData)
|
||||
if err != nil {
|
||||
fmt.Println("Error:", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("Ergebnis: %v\n", result)
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSortOddEven(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected map[string][]int
|
||||
hasError bool
|
||||
}{
|
||||
{
|
||||
input: `[5, 8, 3, 10, 1, 12, 9]`,
|
||||
expected: map[string][]int{
|
||||
"odd": {1, 3, 5, 9},
|
||||
"even": {8, 10, 12},
|
||||
},
|
||||
hasError: false,
|
||||
},
|
||||
{
|
||||
input: `[2, 4, 6]`,
|
||||
expected: map[string][]int{
|
||||
"odd": {},
|
||||
"even": {2, 4, 6},
|
||||
},
|
||||
hasError: false,
|
||||
},
|
||||
{
|
||||
input: `[1, 3, 5]`,
|
||||
expected: map[string][]int{
|
||||
"odd": {1, 3, 5},
|
||||
"even": {},
|
||||
},
|
||||
hasError: false,
|
||||
},
|
||||
{
|
||||
input: `[invalid-json]`,
|
||||
expected: nil,
|
||||
hasError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
result, err := SortOddEven(test.input)
|
||||
|
||||
if test.hasError {
|
||||
if err == nil {
|
||||
t.Errorf("erwarteter Fehler für Eingabe %v, erhielt nil", test.input)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("erwartete keinen Fehler für Eingabe %v, erhielt %v", test.input, err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(result, test.expected) {
|
||||
t.Errorf("für Eingabe %v, erwartete %v, erhielt %v", test.input, test.expected, result)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
package evenOddParrity
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"sort"
|
||||
)
|
||||
|
||||
func JSONToOddEvenMap() {
|
||||
EvenOdd("integers.json")
|
||||
}
|
||||
|
||||
func EvenOdd(filePath string) {
|
||||
// Open the JSON file
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Decode the JSON data
|
||||
var data map[string][]int
|
||||
decoder := json.NewDecoder(file)
|
||||
err = decoder.Decode(&data)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Assume the JSON data contains a key "integers" with an array of integers
|
||||
integers, ok := data["integers"]
|
||||
if !ok {
|
||||
log.Fatal("Key 'integers' not found in JSON")
|
||||
}
|
||||
|
||||
// Sort the integers into even and odd
|
||||
sorted := parity(integers)
|
||||
fmt.Printf("Even numbers: %v\n", sorted["even"])
|
||||
fmt.Printf("Odd numbers: %v\n", sorted["odd"])
|
||||
}
|
||||
|
||||
func parity(integers []int) map[string][]int {
|
||||
sorted := make(map[string][]int)
|
||||
sorted["even"] = []int{}
|
||||
sorted["odd"] = []int{}
|
||||
for _, integer := range integers {
|
||||
if integer%2 == 0 {
|
||||
sorted["even"] = append(sorted["even"], integer)
|
||||
} else {
|
||||
sorted["odd"] = append(sorted["odd"], integer)
|
||||
}
|
||||
}
|
||||
sort.Ints(sorted["even"])
|
||||
sort.Ints(sorted["odd"])
|
||||
return sorted
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
package evenOddParrity
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEvenOdd(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input map[string][]int
|
||||
expectedOutput string
|
||||
}{
|
||||
{
|
||||
name: "Mixed even and odd numbers",
|
||||
input: map[string][]int{
|
||||
"integers": {1, 2, 3, 4, 5, 6},
|
||||
},
|
||||
expectedOutput: "Even numbers: [2 4 6]\nOdd numbers: [1 3 5]\n",
|
||||
},
|
||||
{
|
||||
name: "Only even numbers",
|
||||
input: map[string][]int{
|
||||
"integers": {2, 4, 6, 8, 10},
|
||||
},
|
||||
expectedOutput: "Even numbers: [2 4 6 8 10]\nOdd numbers: []\n",
|
||||
},
|
||||
{
|
||||
name: "Only odd numbers",
|
||||
input: map[string][]int{
|
||||
"integers": {1, 3, 5, 7, 9},
|
||||
},
|
||||
expectedOutput: "Even numbers: []\nOdd numbers: [1 3 5 7 9]\n",
|
||||
},
|
||||
{
|
||||
name: "Empty list",
|
||||
input: map[string][]int{
|
||||
"integers": {},
|
||||
},
|
||||
expectedOutput: "Even numbers: []\nOdd numbers: []\n",
|
||||
},
|
||||
{
|
||||
name: "Single even number",
|
||||
input: map[string][]int{
|
||||
"integers": {2},
|
||||
},
|
||||
expectedOutput: "Even numbers: [2]\nOdd numbers: []\n",
|
||||
},
|
||||
{
|
||||
name: "Single odd number",
|
||||
input: map[string][]int{
|
||||
"integers": {1},
|
||||
},
|
||||
expectedOutput: "Even numbers: []\nOdd numbers: [1]\n",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Mock JSON content
|
||||
mockJSON, err := json.Marshal(tt.input)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal mock data: %v", err)
|
||||
}
|
||||
|
||||
// Create a temporary file
|
||||
tmpFile, err := os.CreateTemp("", "integers.json")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp file: %v", err)
|
||||
}
|
||||
defer os.Remove(tmpFile.Name())
|
||||
|
||||
// Write mock JSON content to the temporary file
|
||||
if _, err := tmpFile.Write(mockJSON); err != nil {
|
||||
t.Fatalf("Failed to write to temp file: %v", err)
|
||||
}
|
||||
if err := tmpFile.Close(); err != nil {
|
||||
t.Fatalf("Failed to close temp file: %v", err)
|
||||
}
|
||||
|
||||
// Redirect stdout to a pipe
|
||||
r, w, _ := os.Pipe()
|
||||
old := os.Stdout
|
||||
os.Stdout = w
|
||||
defer func() {
|
||||
os.Stdout = old
|
||||
r.Close()
|
||||
}()
|
||||
|
||||
// Create a buffer to capture the output
|
||||
var buf bytes.Buffer
|
||||
done := make(chan bool)
|
||||
go func() {
|
||||
buf.ReadFrom(r)
|
||||
done <- true
|
||||
}()
|
||||
|
||||
// Call the evenOdd function
|
||||
EvenOdd(tmpFile.Name())
|
||||
|
||||
// Close the write end of the pipe and wait for the reading to complete
|
||||
w.Close()
|
||||
<-done
|
||||
|
||||
// Verify the output
|
||||
if buf.String() != tt.expectedOutput {
|
||||
t.Errorf("Expected output:\n%s\nGot:\n%s", tt.expectedOutput, buf.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
module sortNumbers
|
||||
|
||||
go 1.23.1
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"integers": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
module 2210806
|
||||
|
||||
go 1.23.1
|
|
@ -0,0 +1,51 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// ein "struct" um die geraden und ungeraden Zahlen zu speichern
|
||||
type Result struct {
|
||||
Odd []int `json:"odd"`
|
||||
Even []int `json:"even"`
|
||||
}
|
||||
|
||||
// oddAndEven nimmt ein JSON Array und gibt ein "Result" zuruck, mit sortierten geraden und ungeraden Zahlen.
|
||||
func oddAndEven(jsonData []byte) (Result, error) {
|
||||
var numbers []int
|
||||
if err := json.Unmarshal(jsonData, &numbers); err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
|
||||
odd := []int{}
|
||||
even := []int{}
|
||||
|
||||
for _, num := range numbers {
|
||||
if num%2 == 0 {
|
||||
even = append(even, num)
|
||||
} else {
|
||||
odd = append(odd, num)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Ints(odd)
|
||||
sort.Ints(even)
|
||||
|
||||
return Result{Odd: odd, Even: even}, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
jsonInput := os.Args[1]
|
||||
|
||||
result, err := oddAndEven([]byte(jsonInput))
|
||||
if err != nil {
|
||||
log.Fatalf("Error processing numbers: %v", err)
|
||||
}
|
||||
|
||||
resultJSON, _ := json.Marshal(result)
|
||||
fmt.Println(string(resultJSON))
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestProcessNumbers(t *testing.T) {
|
||||
tests := []struct {
|
||||
input []int
|
||||
expected Result
|
||||
}{
|
||||
{
|
||||
input: []int{1, 2, 3, 4, 5, 6},
|
||||
expected: Result{Odd: []int{1, 3, 5}, Even: []int{2, 4, 6}},
|
||||
},
|
||||
{
|
||||
input: []int{7, 8, 9, 10, 11, 12},
|
||||
expected: Result{Odd: []int{7, 9, 11}, Even: []int{8, 10, 12}},
|
||||
},
|
||||
{
|
||||
input: []int{0, -1, -2, -3},
|
||||
expected: Result{Odd: []int{-3, -1}, Even: []int{-2, 0}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
jsonInput, _ := json.Marshal(test.input)
|
||||
result, err := oddAndEven(jsonInput)
|
||||
if err != nil {
|
||||
t.Errorf("Error processing input %v: %v", test.input, err)
|
||||
}
|
||||
if !reflect.DeepEqual(result, test.expected) {
|
||||
t.Errorf("For input %v, expected %v, but got %v", test.input, test.expected, result)
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
|
@ -0,0 +1,51 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// ProcessNumbers processes a JSON string of integers
|
||||
func ProcessNumbers(jsonInput string) (map[string][]int, error) {
|
||||
type Input struct {
|
||||
Numbers []int `json:"numbers"`
|
||||
}
|
||||
|
||||
var input Input
|
||||
err := json.Unmarshal([]byte(jsonInput), &input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
numberMap := map[string][]int{
|
||||
"odd": {},
|
||||
"even": {},
|
||||
}
|
||||
|
||||
for _, num := range input.Numbers {
|
||||
if num%2 == 0 {
|
||||
numberMap["even"] = append(numberMap["even"], num)
|
||||
} else {
|
||||
numberMap["odd"] = append(numberMap["odd"], num)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Ints(numberMap["odd"])
|
||||
sort.Ints(numberMap["even"])
|
||||
|
||||
return numberMap, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
jsonInput := `{"numbers": [34, 21, 7, 18, 5, 12, 9, 1, 56, 4]}`
|
||||
numberMap, err := ProcessNumbers(jsonInput)
|
||||
if err != nil {
|
||||
fmt.Println("Error:", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("Sorted Map of Odd and Even Numbers:")
|
||||
fmt.Printf("Odd numbers: %v\n", numberMap["odd"])
|
||||
fmt.Printf("Even numbers: %v\n", numberMap["even"])
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Test ProcessNumbers function
|
||||
func TestProcessNumbers(t *testing.T) {
|
||||
jsonInput := `{"numbers": [34, 21, 7, 18, 5, 12, 9, 1, 56, 4]}`
|
||||
expected := map[string][]int{
|
||||
"odd": {1, 5, 7, 9, 21},
|
||||
"even": {4, 12, 18, 34, 56},
|
||||
}
|
||||
|
||||
result, err := ProcessNumbers(jsonInput)
|
||||
if err != nil {
|
||||
t.Fatalf("Error occurred: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(result, expected) {
|
||||
t.Errorf("Expected %v, but got %v", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
// Test ProcessNumbers with empty input
|
||||
func TestProcessNumbersEmpty(t *testing.T) {
|
||||
jsonInput := `{"numbers": []}`
|
||||
expected := map[string][]int{
|
||||
"odd": {},
|
||||
"even": {},
|
||||
}
|
||||
|
||||
result, err := ProcessNumbers(jsonInput)
|
||||
if err != nil {
|
||||
t.Fatalf("Error occurred: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(result, expected) {
|
||||
t.Errorf("Expected %v, but got %v", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
// Test ProcessNumbers with all odd numbers
|
||||
func TestProcessNumbersAllOdd(t *testing.T) {
|
||||
jsonInput := `{"numbers": [1, 3, 5, 7, 9]}`
|
||||
expected := map[string][]int{
|
||||
"odd": {1, 3, 5, 7, 9},
|
||||
"even": {},
|
||||
}
|
||||
|
||||
result, err := ProcessNumbers(jsonInput)
|
||||
if err != nil {
|
||||
t.Fatalf("Error occurred: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(result, expected) {
|
||||
t.Errorf("Expected %v, but got %v", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
// Test ProcessNumbers with all even numbers
|
||||
func TestProcessNumbersAllEven(t *testing.T) {
|
||||
jsonInput := `{"numbers": [2, 4, 6, 8, 10]}`
|
||||
expected := map[string][]int{
|
||||
"odd": {},
|
||||
"even": {2, 4, 6, 8, 10},
|
||||
}
|
||||
|
||||
result, err := ProcessNumbers(jsonInput)
|
||||
if err != nil {
|
||||
t.Fatalf("Error occurred: %v", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(result, expected) {
|
||||
t.Errorf("Expected %v, but got %v", expected, result)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
module 2212765
|
||||
|
||||
go 1.23.1
|
|
@ -0,0 +1,24 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"2212765/numbers"
|
||||
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
json, error := os.ReadFile("numbers.json")
|
||||
if error != nil {
|
||||
fmt.Println(error)
|
||||
return
|
||||
}
|
||||
|
||||
numbersMap, error := numbers.JSONToOddEvenMap(string(json))
|
||||
if error != nil {
|
||||
fmt.Println("JSONToOddEvenMap returned error: ", error)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Even: %v\nOdd: %v\n", numbersMap["even"], numbersMap["odd"])
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
[8, 1, 42, 99, 69, 88, 3, 7]
|
|
@ -0,0 +1,31 @@
|
|||
package numbers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sort"
|
||||
)
|
||||
|
||||
func JSONToOddEvenMap(jsonNumbers string) (map[string][]int, error) {
|
||||
var numbers []int
|
||||
|
||||
if error := json.Unmarshal([]byte(jsonNumbers), &numbers); error != nil {
|
||||
return nil, error
|
||||
}
|
||||
|
||||
result := map[string][]int{
|
||||
"even": {},
|
||||
"odd": {},
|
||||
}
|
||||
|
||||
sort.Ints(numbers)
|
||||
|
||||
for _, number := range numbers {
|
||||
if number%2 == 0 {
|
||||
result["even"] = append(result["even"], number)
|
||||
} else {
|
||||
result["odd"] = append(result["odd"], number)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
package numbers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type TestCase struct {
|
||||
input string
|
||||
expected string
|
||||
}
|
||||
|
||||
var testCases = []TestCase{
|
||||
{
|
||||
input: "[1, 2, 3, 4, 5]",
|
||||
expected: `{"even":[2,4],"odd":[1,3,5]}`,
|
||||
},
|
||||
{
|
||||
input: "[5, 4, 3, 2, 1]",
|
||||
expected: `{"even":[2,4],"odd":[1,3,5]}`,
|
||||
},
|
||||
{
|
||||
input: "[1, 3, 5, 7, 9]",
|
||||
expected: `{"even":[],"odd":[1,3,5,7,9]}`,
|
||||
},
|
||||
{
|
||||
input: "[2, 4, 6, 8, 10]",
|
||||
expected: `{"even":[2,4,6,8,10],"odd":[]}`,
|
||||
},
|
||||
{
|
||||
input: "[]",
|
||||
expected: `{"even":[],"odd":[]}`,
|
||||
},
|
||||
}
|
||||
|
||||
func TestJSONToOddEvenMap(t *testing.T) {
|
||||
for _, testCase := range testCases {
|
||||
result, error := JSONToOddEvenMap(testCase.input)
|
||||
|
||||
if error != nil {
|
||||
t.Errorf("JSONToOddEvenMap(%q) returned error: %v",
|
||||
testCase.input,
|
||||
error)
|
||||
}
|
||||
|
||||
resultJSON, _ := json.Marshal(result)
|
||||
|
||||
if string(resultJSON) != testCase.expected {
|
||||
t.Errorf("JSONToOddEvenMap(%q) returned %s, but %s was expected",
|
||||
testCase.input,
|
||||
resultJSON,
|
||||
testCase.expected)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
)
|
||||
|
||||
func main() {
|
||||
input := `[5, 3, 8, 1, 4, 10, 9]`
|
||||
result, err := NumberProcess(input)
|
||||
if err != nil {
|
||||
fmt.Println("Error:", err)
|
||||
return
|
||||
}
|
||||
fmt.Println(result)
|
||||
}
|
||||
|
||||
func NumberProcess(numbers string) (map[string][]int, error) {
|
||||
var intArr []int
|
||||
err := json.Unmarshal([]byte(numbers), &intArr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := map[string][]int{
|
||||
"odd": {},
|
||||
"even": {},
|
||||
}
|
||||
|
||||
for _, num := range intArr {
|
||||
if num%2 == 0 {
|
||||
result["even"] = append(result["even"], num)
|
||||
} else {
|
||||
result["odd"] = append(result["odd"], num)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Ints(result["odd"])
|
||||
sort.Ints(result["even"])
|
||||
|
||||
return result, nil
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestJSONtoOddEvenMap(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected map[string][]int
|
||||
hasError bool
|
||||
}{
|
||||
{
|
||||
input: `[1, 2, 3, 4, 5]`,
|
||||
expected: map[string][]int{
|
||||
"odd": {1, 3, 5},
|
||||
"even": {2, 4},
|
||||
},
|
||||
hasError: false,
|
||||
},
|
||||
{
|
||||
input: `[-1, -2, 0, 7, 8]`,
|
||||
expected: map[string][]int{
|
||||
"odd": {-1, 7},
|
||||
"even": {-2, 0, 8},
|
||||
},
|
||||
hasError: false,
|
||||
},
|
||||
{
|
||||
input: `[1, "abc", 3]`, // Fehlerfall: falsches JSON
|
||||
expected: nil,
|
||||
hasError: true,
|
||||
},
|
||||
{
|
||||
input: `[]`,
|
||||
expected: map[string][]int{
|
||||
"odd": {},
|
||||
"even": {},
|
||||
},
|
||||
hasError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
result, err := NumberProcess(test.input)
|
||||
if (err != nil) != test.hasError {
|
||||
t.Errorf("Fehler erwartet = %v, erhalten = %v", test.hasError, err)
|
||||
}
|
||||
if !reflect.DeepEqual(result, test.expected) {
|
||||
t.Errorf("Erwartet %v, erhalten %v für Eingabe %v", test.expected, result, test.input)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
module 3001302
|
||||
|
||||
go 1.22.5
|
|
@ -0,0 +1,34 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"3001302/numbers"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Open the numbers.json file
|
||||
data, err := os.ReadFile("./numbers/numbers.json")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Unmarshal the JSON data
|
||||
var nums []int
|
||||
err = json.Unmarshal(data, &nums)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
result := numbers.CategorizeNumbers(nums)
|
||||
|
||||
numbers.SortNumbers(result)
|
||||
|
||||
// Print the result
|
||||
fmt.Printf("Unmarshaled: even:[%s] odd:[%s]\n",
|
||||
numbers.FormatSlice(result["even"]),
|
||||
numbers.FormatSlice(result["odd"]))
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package numbers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// separate integers by comma
|
||||
func FormatSlice(s []int) string {
|
||||
var result string
|
||||
for i, v := range s {
|
||||
if i > 0 {
|
||||
result += ", "
|
||||
}
|
||||
result += fmt.Sprintf("%d", v)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// categorize numbers into even and odd
|
||||
func CategorizeNumbers(nums []int) map[string][]int {
|
||||
result := map[string][]int{"even": {}, "odd": {}}
|
||||
for _, n := range nums {
|
||||
if n%2 == 0 {
|
||||
result["even"] = append(result["even"], n)
|
||||
} else {
|
||||
result["odd"] = append(result["odd"], n)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// sort the slices in ascending order
|
||||
func SortNumbers(result map[string][]int) {
|
||||
sort.Ints(result["even"])
|
||||
sort.Ints(result["odd"])
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
[
|
||||
1,
|
||||
5,
|
||||
6,
|
||||
9,
|
||||
10,
|
||||
13,
|
||||
50,
|
||||
53,
|
||||
99
|
||||
]
|
|
@ -0,0 +1,110 @@
|
|||
// numbers_test.go
|
||||
package numbers
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type FormatSliceTestCase struct {
|
||||
input []int
|
||||
expected string
|
||||
}
|
||||
|
||||
var formatSliceTestCases = []FormatSliceTestCase{
|
||||
{
|
||||
input: []int{},
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
input: []int{1},
|
||||
expected: "1",
|
||||
},
|
||||
{
|
||||
input: []int{1, 2, 3},
|
||||
expected: "1, 2, 3",
|
||||
},
|
||||
}
|
||||
|
||||
func TestFormatSlice(t *testing.T) {
|
||||
for _, testCase := range formatSliceTestCases {
|
||||
result := FormatSlice(testCase.input)
|
||||
|
||||
if result != testCase.expected {
|
||||
t.Errorf("FormatSlice(%v) returned %q, but %q was expected",
|
||||
testCase.input,
|
||||
result,
|
||||
testCase.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type CategorizeNumbersTestCase struct {
|
||||
input []int
|
||||
expected map[string][]int
|
||||
}
|
||||
|
||||
var categorizeNumbersTestCases = []CategorizeNumbersTestCase{
|
||||
{
|
||||
input: []int{},
|
||||
expected: map[string][]int{"even": {}, "odd": {}},
|
||||
},
|
||||
{
|
||||
input: []int{2, 4, 6},
|
||||
expected: map[string][]int{"even": {2, 4, 6}, "odd": {}},
|
||||
},
|
||||
{
|
||||
input: []int{1, 3, 5},
|
||||
expected: map[string][]int{"even": {}, "odd": {1, 3, 5}},
|
||||
},
|
||||
{
|
||||
input: []int{1, 2, 3, 4, 5, 6},
|
||||
expected: map[string][]int{"even": {2, 4, 6}, "odd": {1, 3, 5}},
|
||||
},
|
||||
}
|
||||
|
||||
func TestCategorizeNumbers(t *testing.T) {
|
||||
for _, testCase := range categorizeNumbersTestCases {
|
||||
result := CategorizeNumbers(testCase.input)
|
||||
|
||||
if !reflect.DeepEqual(result, testCase.expected) {
|
||||
t.Errorf("CategorizeNumbers(%v) returned %+v, but %+v was expected",
|
||||
testCase.input,
|
||||
result,
|
||||
testCase.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type SortNumbersTestCase struct {
|
||||
input map[string][]int
|
||||
expected map[string][]int
|
||||
}
|
||||
|
||||
var sortNumbersTestCases = []SortNumbersTestCase{
|
||||
{
|
||||
input: map[string][]int{"even": {}, "odd": {}},
|
||||
expected: map[string][]int{"even": {}, "odd": {}},
|
||||
},
|
||||
{
|
||||
input: map[string][]int{"even": {4, 2, 6}, "odd": {3, 1, 5}},
|
||||
expected: map[string][]int{"even": {2, 4, 6}, "odd": {1, 3, 5}},
|
||||
},
|
||||
{
|
||||
input: map[string][]int{"even": {2, 4, 6}, "odd": {1, 3, 5}},
|
||||
expected: map[string][]int{"even": {2, 4, 6}, "odd": {1, 3, 5}},
|
||||
},
|
||||
}
|
||||
|
||||
func TestSortNumbers(t *testing.T) {
|
||||
for _, testCase := range sortNumbersTestCases {
|
||||
SortNumbers(testCase.input)
|
||||
|
||||
if !reflect.DeepEqual(testCase.input, testCase.expected) {
|
||||
t.Errorf("SortNumbers(%+v) returned %+v, but %+v was expected",
|
||||
testCase.input,
|
||||
testCase.input,
|
||||
testCase.expected)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
module 3001327
|
||||
|
||||
go 1.23.1
|
|
@ -0,0 +1,42 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
)
|
||||
|
||||
func Seperator(input string) (map[string][]int, error) {
|
||||
var numbers []int
|
||||
err := json.Unmarshal([]byte(input), &numbers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
numMap := map[string][]int{
|
||||
"even": {},
|
||||
"odd": {},
|
||||
}
|
||||
|
||||
for _, value := range numbers {
|
||||
if value%2 == 0 {
|
||||
numMap["even"] = append(numMap["even"], value)
|
||||
} else {
|
||||
numMap["odd"] = append(numMap["odd"], value)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Ints(numMap["even"])
|
||||
sort.Ints(numMap["odd"])
|
||||
|
||||
fmt.Println(numMap)
|
||||
return numMap, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Define the input slice
|
||||
input := `[10, 9, 8, 7, 6, 5, 4, 3, 2, 1]`
|
||||
|
||||
// Call the Seperator function
|
||||
Seperator(input)
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var example = []struct {
|
||||
input string
|
||||
expected map[string][]int
|
||||
}{
|
||||
{
|
||||
input: `[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]`,
|
||||
expected: map[string][]int{
|
||||
"even": {2, 4, 6, 8, 10},
|
||||
"odd": {1, 3, 5, 7, 9},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `[10, 9, 8, 7, 6, 5, 4, 3, 2, 1]`,
|
||||
expected: map[string][]int{
|
||||
"even": {2, 4, 6, 8, 10},
|
||||
"odd": {1, 3, 5, 7, 9},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `[ -6, -5, -4, -3, -2, -1, 1, 2, 3, 4]`,
|
||||
expected: map[string][]int{
|
||||
"even": {-6, -4, -2, 2, 4},
|
||||
"odd": {-5, -3, -1, 1, 3},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `[10, 8, 8, 7, 6, 6, 4, 3, 2, 1]`,
|
||||
expected: map[string][]int{
|
||||
"even": {2, 4, 6, 6, 8, 8, 10},
|
||||
"odd": {1, 3, 7},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `[10, 8, 6, 4, 2]`,
|
||||
expected: map[string][]int{
|
||||
"even": {2, 4, 6, 8, 10},
|
||||
"odd": {},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `[9, 7, 5, 3, 1]`,
|
||||
expected: map[string][]int{
|
||||
"even": {},
|
||||
"odd": {1, 3, 5, 7, 9},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `[]`,
|
||||
expected: map[string][]int{
|
||||
"even": {},
|
||||
"odd": {},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestSeperator(t *testing.T) {
|
||||
for _, singleCase := range example {
|
||||
result, err := Seperator(singleCase.input)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(result, singleCase.expected) {
|
||||
t.Errorf("For input %v, expected %v but got %v", singleCase.input, singleCase.expected, result)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
module 3001838
|
||||
|
||||
go 1.23.1
|
|
@ -0,0 +1,35 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
)
|
||||
|
||||
func ProcessNumbers(numbers string) (map[string][]int, error) {
|
||||
|
||||
var intArrNrs []int
|
||||
e := json.Unmarshal([]byte(numbers), &intArrNrs)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
|
||||
result := map[string][]int{
|
||||
"odd": {},
|
||||
"even": {},
|
||||
}
|
||||
|
||||
for _, num := range intArrNrs {
|
||||
if num%2 == 0 {
|
||||
result["even"] = append(result["even"], num)
|
||||
} else {
|
||||
result["odd"] = append(result["odd"], num)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Ints(result["odd"])
|
||||
sort.Ints(result["even"])
|
||||
fmt.Println(result)
|
||||
|
||||
return result, nil
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestProcessNumbers(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected map[string][]int
|
||||
hasError bool
|
||||
}{
|
||||
{
|
||||
input: `[1, 4, 3, 10, 5, 2, 9, 7, 8, 6]`,
|
||||
expected: map[string][]int{
|
||||
"odd": {1, 3, 5, 7, 9},
|
||||
"even": {2, 4, 6, 8, 10},
|
||||
},
|
||||
hasError: false,
|
||||
},
|
||||
{
|
||||
input: `[]`,
|
||||
expected: map[string][]int{
|
||||
"odd": {},
|
||||
"even": {},
|
||||
},
|
||||
hasError: false,
|
||||
},
|
||||
{
|
||||
input: `[1, "two", 3]`,
|
||||
expected: nil,
|
||||
hasError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range tests {
|
||||
t.Logf("Running test case %d with input: %s", i+1, tc.input)
|
||||
result, err := ProcessNumbers(tc.input)
|
||||
|
||||
if tc.hasError {
|
||||
if err == nil {
|
||||
t.Errorf("expected an error for input %v, but got none", tc.input)
|
||||
} else {
|
||||
t.Logf("Expected error received for input: %s", tc.input)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error for input %v: %v", tc.input, err)
|
||||
} else {
|
||||
t.Logf("Result: %v", result)
|
||||
}
|
||||
if !reflect.DeepEqual(result, tc.expected) {
|
||||
t.Errorf("expected %v, got %v", tc.expected, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
module 3002102
|
||||
|
||||
go 1.23.1
|
|
@ -0,0 +1,34 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
)
|
||||
|
||||
func OddEvenMap(Input string) (map[string][]int, error) {
|
||||
var numbers []int
|
||||
err := json.Unmarshal([]byte(Input), &numbers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := map[string][]int{
|
||||
"odd": {},
|
||||
"even": {},
|
||||
}
|
||||
|
||||
for _, num := range numbers {
|
||||
if num%2 == 0 {
|
||||
result["even"] = append(result["even"], num)
|
||||
} else {
|
||||
result["odd"] = append(result["odd"], num)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Ints(result["odd"])
|
||||
sort.Ints(result["even"])
|
||||
|
||||
fmt.Println(result)
|
||||
return result, nil
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestOddEvenMapJSON(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected map[string][]int
|
||||
}{
|
||||
{
|
||||
input: `[5, 2, 9, 8, 3, 4, 7, 6, 1]`,
|
||||
expected: map[string][]int{
|
||||
"odd": {1, 3, 5, 7, 9},
|
||||
"even": {2, 4, 6, 8},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `[10, 15, 20, 25]`,
|
||||
expected: map[string][]int{
|
||||
"odd": {15, 25},
|
||||
"even": {10, 20},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `[]`,
|
||||
expected: map[string][]int{
|
||||
"odd": {},
|
||||
"even": {},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
result, err := OddEvenMap(test.input)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(result, test.expected) {
|
||||
t.Errorf("For input %v, expected %v but got %v", test.input, test.expected, result)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
module numbers
|
||||
|
||||
go 1.23.1
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue