Go-知识测试-单元测试

news2024/10/7 6:37:58

Go-知识测试-单元测试

  • 1. 定义
  • 2. 使用
  • 3. testing.common 测试基础数据
  • 4. testing.TB 接口
  • 5. 单元测试的原理
    • 5.1 context 单元测试的调度
      • 5.1.1 等待并发执行 testContext.waitParallel
      • 5.1.2 并发测试结束 testContext.release
    • 5.2 测试执行 tRunner
    • 5.3 启动测试 Run
    • 5.4 启动并发测试 Parallel

1. 定义

单元测试是指对软件中的最小可测试单元进行检查和验证,比如对一个函数的测试。
单元测试要保证测试文件以_test.go结尾。
测试方法必须以TestXxx开头。
测试文件可以与源码处于同一目录,也可以处于单独的目录。

2. 使用

比如
在这里插入图片描述

执行单个TestXxx
在这里插入图片描述

执行整个测试文件
在这里插入图片描述

使用 go test 即可触发测试

3. testing.common 测试基础数据

每个单元测试都有一个入参t *testing.T,结构定义如下:

type T struct {
	common
	isEnvSet bool
	context  *testContext // For running tests and subtests.
}

T 组合了 common 类型

// common包含T和B之间的公共元素,以及
// 捕获常见的方法,如Errorf。
type common struct {
	mu          sync.RWMutex         // 保卫这群田地
	output      []byte               // 测试或基准测试生成的输出。
	w           io.Writer            // 对于flushToParent。
	ran         bool                 // 执行了测试或基准测试(或其中一个子测试)。
	failed      bool                 // 测试或基准测试失败。
	skipped     bool                 // 已跳过测试或基准测试。
	done        bool                 // 测试已完成,所有子测试均已完成。
	helperPCs   map[uintptr]struct{} // 写入文件/行信息时要跳过的函数
	helperNames map[string]struct{}  // helperPC转换为函数名
	cleanups    []func()             // 测试结束时要调用的可选函数
	cleanupName string               // 清除函数的名称。
	cleanupPc   []uintptr            // 调用Cleanup的点处的堆栈跟踪。
	finished    bool                 // 测试功能已完成。
	chatty      *chattyPrinter       // 如果设置了chatty标志,则为chattyPrinter的副本。
	bench       bool                 // 当前测试是否为基准测试。
	hasSub      int32                // 以原子形式书写。
	raceErrors  int                  // 测试过程中检测到的种族数。
	runner      string               // 运行测试的tRunner的函数名称。
	parent      *common
	level       int       // 测试或基准的嵌套深度。
	creator     []uintptr // 如果级别>0,则堆栈跟踪父级调用t.Run的点。
	name        string    // 测试或基准的名称。
	start       time.Time // 时间测试或基准测试已启动
	duration    time.Duration
	barrier     chan bool // 为了发出平行子测验的信号,他们可以开始。
	signal      chan bool // 发出测试完成的信号。
	sub         []*T      // 要并行运行的子测试的队列。
	tempDirMu   sync.Mutex
	tempDir     string
	tempDirErr  error
	tempDirSeq  int32
}

每个测试均对应一个 testing.common,不仅记录了测试函数的基础信息(比如名字),还管理了测试的执行过程和测试结果。
testing.commong是单元测试,性能测试和模糊测试的基础。
通过继承共同的结构,保证了各种测试的行为一致,降低使用的门槛。

4. testing.TB 接口

testing.common 实现的接口为 testing.TB,单元测试和性能测试通过该接口获取基础能力。

type TB interface {
	Cleanup(func())                            // 清理
	Error(args ...interface{})                 // 表示测试失败+记录日志
	Errorf(format string, args ...interface{}) // 格式化表示测试失败+记录日志
	Fail()                                     // 表示测试失败
	FailNow()                                  // 标记测试失败+结束当前测试
	Failed() bool                              // 查询结果
	Fatal(args ...interface{})                 // 标记测试失败+记录日志+结束当前测试
	Fatalf(format string, args ...interface{}) // 格式化标记测试失败+记录日志+结束当前测试
	Helper()                                   // 标记测试为 Helper (避免打印当前代码行号)
	Log(args ...interface{})                   // 记录日志
	Logf(format string, args ...interface{})   // 格式化 记录日志
	Name() string                              // 查询测试名
	Setenv(key, value string)                  // 设置环境变量
	Skip(args ...interface{})                  // 记录日志+跳过测试
	SkipNow()                                  // 跳过测试
	Skipf(format string, args ...interface{})  // 格式化记录日志+跳过测试
	Skipped() bool                             // 查询测试是否被跳过
	TempDir() string                           // 返回一个临时目录
	//阻止用户实现的私有方法
	//接口,因此将来不会添加
	//违反Go 1兼容性。
	private()
}

实际上单元测试,性能测试和模糊测试都会使用接口。

接口定义中的 private() 方法是一个很巧妙的用法
目的是限定 testing.TB 接口全局唯一,防止用户自行实现 testing.TB接口
类似Java 的 final 关键字
因为 private 是小写的,包内可见,这样用户就无法实现 private 方法
用户也就无法实现 testing.TB 接口了

5. 单元测试的原理

单元测试的函数的入参是testing.T类型:

type T struct {
	common
	isParallel bool         // 是否需要并发
	isEnvSet   bool         // 是否设置环境变量
	context    *testContext //用于运行测试和子测试。
}

5.1 context 单元测试的调度

context 是调度单元测试的关键:

// testContext包含所有测试通用的所有字段。这包括
// 同步原语最多运行*个并行测试。
type testContext struct {
	match         *matcher   // 匹配器,用于管理测试名称匹配、过滤等
	deadline      time.Time  // 结束时间,防止测试运行过长
	mu            sync.Mutex // 互斥锁,用于控制 testContext 成员的互斥访问
	startParallel chan bool  // 用于向准备并行运行的测试发出信号的通道。
	running       int        // running是当前并行运行的测试数。这不包括等待子测验完成的测验。
	numWaiting    int        // numWaiting是等待并行运行的测试数。
	maxParallel   int        // maxParallel是并行标志的副本。
}

其初始化代码

func newTestContext(maxParallel int, m *matcher) *testContext {
	return &testContext{
		match:         m,
		startParallel: make(chan bool),
		maxParallel:   maxParallel,
		running:       1, // 设置主测试
	}
}

5.1.1 等待并发执行 testContext.waitParallel

如果一个测试使用t.Parallel()启动并发,那么这个测试并不是立即被并发执行,需要检查当前
并发执行的测试数是否达到最大值,这个检查工作统一放在testContext.waitParallel()中实现

func (c *testContext) waitParallel() {
    // 加锁
	c.mu.Lock()
	// 判断是否达到最大值
	if c.running < c.maxParallel {
	    // 如果没有达到最大值,那么 运行数++
		c.running++
		c.mu.Unlock()
		return
	}
	// 如果已经达到最大值,那么等待执行的数量++
	c.numWaiting++
	c.mu.Unlock()
	// 获取启动的令牌信号,需要注意,chan 是没有缓冲区的
	// 也就是会阻塞,直到有生产 的 chan ,才会解除阻塞
	<-c.startParallel
}

阻塞等待后面没有累加 running ,因为 running 表示的运行中的个数
阻塞解除,必然表示一个测试结束,当前测试开始,运行中的个数没有变化,所以不增加

5.1.2 并发测试结束 testContext.release

当并发测试结束后,会通过 release 方法释放一个信号,用于启动其他等待并发测试的函数

func (c *testContext) release() {
    // 加锁
	c.mu.Lock()
	// 如果没有等待启动的测试,那么直接将 running-- 
	if c.numWaiting == 0 {
		c.running--
		c.mu.Unlock()
		return
	}
	// 否则将等待的数量减1,然后生产一个启动的信号
	c.numWaiting--
	c.mu.Unlock()
	c.startParallel <- true // Pick a waiting test to be run.
}

5.2 测试执行 tRunner

func tRunner(t *T, fn func(t *T)) {
	t.runner = callerName(0) // 获取当前测试函数的名称
	//当这个goroutine完成时,要么是因为fn(t)
	//正常返回或由于触发测试失败
	//对运行时的调用。Goexit,记录持续时间并发送
	//表示测试完成的信号。
	defer func() {
		// 测试失败,那么将失败数+1
		if t.Failed() {
			atomic.AddUint32(&numFailed, 1)
		}
		// 如果测试惊慌失措,请在终止之前打印任何测试输出。
		err := recover()
		signal := true
		// 读锁定
		t.mu.RLock()
		// 获取完成状态
		finished := t.finished
		// 读锁定解锁
		t.mu.RUnlock()
		// 如果测试未完成,但是异常信息为空
		if !finished && err == nil {
			// 将错误信息赋值为空错误或空异常
			err = errNilPanicOrGoexit
			// 如果有父测试,当前是子测试
			for p := t.parent; p != nil; p = p.parent {
				p.mu.RLock()
				finished = p.finished
				p.mu.RUnlock()
				if finished {
					t.Errorf("%v: subtest may have called FailNow on a parent test", err)
					err = nil
					signal = false
					break
				}
			}
		}
		// 使用延迟调用以确保我们报告测试
		// 完成,即使清除函数调用t.FailNow。请参见第41355期。
		didPanic := false
		defer func() {
			if didPanic {
				return
			}
			if err != nil {
				panic(err)
			}
			//只有在没有恐慌的情况下才报告测试完成,
			//否则,测试二进制文件可以在死机之前退出
			//报告给用户。请参见第41479期。
			t.signal <- signal
		}()

		doPanic := func(err interface{}) {
			// 设置测试失败
			t.Fail()
			if r := t.runCleanup(recoverAndReturnPanic); r != nil {
				t.Logf("cleanup panicked with %v", r)
			}
			//在终止之前将输出日志刷新到根目录。
			for root := &t.common; root.parent != nil; root = root.parent {
				root.mu.Lock()
				// 计算时间
				root.duration += time.Since(root.start)
				d := root.duration
				root.mu.Unlock()
				root.flushToParent(root.name, "--- FAIL: %s (%s)\n", root.name, fmtDuration(d))
				if r := root.parent.runCleanup(recoverAndReturnPanic); r != nil {
					fmt.Fprintf(root.parent.w, "cleanup panicked with %v", r)
				}
			}
			didPanic = true
			panic(err)
		}
		if err != nil {
			doPanic(err)
		}

		t.duration += time.Since(t.start)
		// 如果有子测试,当前是父测试
		if len(t.sub) > 0 {
			// 停止测试
			t.context.release()
			// 释放平行的子测验。
			close(t.barrier)
			// 等待子测验完成。
			for _, sub := range t.sub {
				<-sub.signal
			}
			cleanupStart := time.Now()
			err := t.runCleanup(recoverAndReturnPanic)
			t.duration += time.Since(cleanupStart)
			if err != nil {
				doPanic(err)
			}
			// 如果不是并发的
			if !t.isParallel {
				// 等待开始
				t.context.waitParallel()
			}
		} else if t.isParallel { // 如果是并发的
			//仅当此测试以并行方式运行时才释放其计数 测验请参阅Run方法中的注释。
			t.context.release()
		}
		// 测试执行结束上报日志
		t.report()
		t.done = true
		// 如果有父测试,那么设置执行标志
		if t.parent != nil && atomic.LoadInt32(&t.hasSub) == 0 {
			t.setRan()
		}
	}()
	defer func() {
		if len(t.sub) == 0 {
			t.runCleanup(normalPanic)
		}
	}()

	t.start = time.Now()
	t.raceErrors = -race.Errors()
	fn(t)

	// code beyond here will not be executed when FailNow is invoked
	t.mu.Lock()
	t.finished = true
	t.mu.Unlock()
}

测试命令go test首先会触发RunTests方法
在这里插入图片描述

然后会根据传入参数进行启动
在这里插入图片描述

所以在 tRunner 中的 fn 就是 一个 for 循环,循环启动测试case
所以也就是说 tRunner 是一个公共的方法,不管是单元测试,还是性能测试,还是模糊测试,都会调用tRunner
这也是为何tRunner方法中混杂了性能测试,子测试的逻辑。
tRunner 传递一个经调度这设置过的 testing.T 参数和一个测试函数,执行时记录开始时间,
然后将testing.T 参数传入测试函数并同步等待结束。
tRunner在defer语句中记录测试执行耗时,并上报日志,最后发送结束信号。

5.3 启动测试 Run

在调用顺序中,调用tRunner的是Run

// 将运行f作为名为name的t的子测试。它在一个单独的goroutine中运行f
// 并且阻塞直到f返回或调用t。并行成为并行测试。
// 运行报告f是否成功(或者至少在调用t.Parallel之前没有失败)。
//
// Run可以从多个goroutine同时调用,但所有此类调用
// 必须在t的外部测试函数返回之前返回。
func (t *T) Run(name string, f func(t *T)) bool {
	// 将子测试的数量+1
	atomic.StoreInt32(&t.hasSub, 1)
	// 获取匹配的测试name
	testName, ok, _ := t.context.match.fullName(&t.common, name)
	// 如果没有配置,那么直接结束
	if !ok || shouldFailFast() {
		return true
	}
	//记录此调用点的堆栈跟踪,以便如果子测试
	//在单独的堆栈中运行的函数被标记为助手,我们可以
	//继续将堆栈遍历到父测试中。
	var pc [maxStackLen]uintptr
	// 获取调用者的函数name
	n := runtime.Callers(2, pc[:])
	t = &T{ // 创建一个新的 testing.T 用于执行子测试
		common: common{
			barrier: make(chan bool),
			signal:  make(chan bool, 1),
			name:    testName,
			parent:  &t.common,
			level:   t.level + 1,
			creator: pc[:n],
			chatty:  t.chatty,
		},
		context: t.context,
	}
	t.w = indenter{&t.common}
	if t.chatty != nil {
		t.chatty.Updatef(t.name, "=== RUN   %s\n", t.name)
	}
	//而不是在调用之前减少此测试的运行计数
	//tRunner并在之后增加它,我们依靠tRunner保持
	//计数正确。这样可以确保运行一系列顺序测试
	//而不会被抢占,即使它们的父级是并行测试。这
	//如果*parallel==1,则可以特别减少意外。
	go tRunner(t, f)
	if !<-t.signal {
		//此时,FailNow很可能是在
		//其中一个子测验的家长测验。继续中止链的上行。
		runtime.Goexit()
	}
	return !t.failed
}

Run函数启动一个单独的协程来运行名字为name的子测试f,并且会阻塞等待其执行结束,除非子测试f显式
调用t.Parallel将自己变为一个可并行的测试,最后返回 bool 类型的测试结果。
比如,挡在测试 func TestXxx(t *testing.T) 中调用 Run(name, f)时,
Run将启动一个名为TestXxx/name的子测试。
另外,所有的测试,包括 func TestXxx(t *testing.T) 自身,都是由testMain使用Run方法启动。
每启动一个子测试都会创建一个testing.T变量,该变量继承当前测试的部分属性,然后以新协程去执行,当前测试会在子测试结束后返回子测试的结果。
子测试退出条件要么是子测试执行结束,要么是子测试设置了Parallel,否则是异常退出。

5.4 启动并发测试 Parallel

Parallel方法将当前测试已加入并发队列

// 与此测试并行运行的并行信号(且仅与)
// 其他平行测试。当测试由于使用而多次运行时
// -test.count或-test.cpu,单个测试的多个实例从未在中运行
// 彼此平行。
func (t *T) Parallel() {
	// 重复调用
	if t.isParallel {
		panic("testing: t.Parallel called multiple times")
	}
	// 先设置环境变量
	if t.isEnvSet {
		panic("testing: t.Parallel called after t.Setenv; cannot set environment variables in parallel tests")
	}
	t.isParallel = true
	//我们不想把等待串行测试的时间包括在内
	//在测试持续时间内。记录到目前为止经过的时间,并重置
	//计时器之后。
	t.duration += time.Since(t.start)
	//添加到要由父级发布的测试列表中。
	t.parent.sub = append(t.parent.sub, t)
	t.raceErrors += race.Errors()
	if t.chatty != nil {
		//不幸的是,即使PAUSE表示命名测试是*no
		//运行时间较长*,cmd/test2json将其解释为更改活动测试
		//用于日志解析。我们可以修复cmd/test2json,但是
		//不会修复已经shell的第三方工具的现有部署
		//向外扩展到cmd/test2json的旧版本——因此仅修复cmd/test1json
		//目前还不够。
		t.chatty.Updatef(t.name, "=== PAUSE %s\n", t.name)
	}
	// 当前测试即将进入并发模式,表示测试结束,这样父测试就不会等待并退出 Run
	t.signal <- true   // 释放调用测试。
	<-t.parent.barrier // 等待父测试完成。
	// 阻塞等待并发调度
	t.context.waitParallel()
	if t.chatty != nil {
		t.chatty.Updatef(t.name, "=== CONT  %s\n", t.name)
	}
	// 重置时间,第二段耗时
	t.start = time.Now()
	t.raceErrors += -race.Errors()
}

在testContext中,启动一个并发测试测试后,当并发数达到最大时,并不会马上开始执行,
而是需要等待一个测试执行完成后,才会启动一个等待的测试,等待时间不会计入测试的执行耗时中。
在Run里面,启动一个测试后,会等待测试执行完成后,才会启动下一个。 如果是并发,那么就不需要等待了。
通过 t.signal 通知父测试,父测试从Run中唤醒,继续执行。
父测试唤醒后继续执行,结束后进入defer,在defer中将启动所有子测试,并等待子测试执行结束。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1876191.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

《晨集》开源软件平台的创新与发展

一、引言 在数字化浪潮的推动下&#xff0c;开源软件平台已成为推动软件创新、促进知识共享的重要力量。《晨集》作为新兴的开源软件平台&#xff0c;其上线标志着开源生态圈的又一重要里程碑。本文旨在探讨《晨集》开源软件平台的创新特点、对开发者社区的影响以及未来发展趋…

Flink作业执行之 4.JobGraph

Flink作业执行之 4.JobGraph 1. 入口 前文了解了由Transformation到StreamGraph的过程&#xff0c;StreamGraph即作业的逻辑拓扑结构。 生成逻辑结构后&#xff0c;接下来的操作往往是对逻辑结构的优化。在很多组件中都是这样的处理&#xff0c;如hive、spark等都会执行“逻辑…

【linux】从零到入门

linux概述 Linux是一个免费使用和自由传播的一套操作系统。用户可以无偿地得到它地源代码&#xff0c;和大量地应用程序&#xff0c;并且可以随意修改和增加它们。 Linux的内核起初由林纳斯编写。内核是啥&#xff1f; 驱动设备&#xff0c;文件系统&#xff0c;进程管理&…

『MySQL 实战 45 讲』22 - MySQL 有哪些“饮鸩止渴”提高性能的方法?

MySQL 有哪些“饮鸩止渴”提高性能的方法&#xff1f; 需求&#xff1a;业务高峰期&#xff0c;生产环境的 MySQL 压力太大&#xff0c;没法正常响应&#xff0c;需要短期内、临时性地提升一些性能 短连接风暴 短连接模式&#xff1a;执行很少的 SQL 语句就断开&#xff0c;…

【Sklearn-驯化】一文搞懂机器学习树模型建模可视化过程

【Sklearn-驯化】一文搞懂机器学习树模型建模可视化过程 本次修炼方法请往下查看 &#x1f308; 欢迎莅临我的个人主页 &#x1f448;这里是我工作、学习、实践 IT领域、真诚分享 踩坑集合&#xff0c;智慧小天地&#xff01; &#x1f387; 免费获取相关内容文档关注&#xff…

LeetCode刷题之HOT100之数组中的第K个最大元素

2024 6/29 今天天气很好啊&#xff0c;想爬山&#xff0c;奈何下午还有最后的一个汇报。做个题先 1、题目描述 2、算法分析 看到这个题我想到的就是: public int findKthLargest(int[] nums, int k) {Arrays.sort(nums);return nums[nums.length - k ];}哈哈&#xff0c;我提…

计算机网络 —— 基本概念

基本概念 1. 通信协议2. 面向连接 v.s. 面向无连接3. 电路交换 v.s. 分组交换4. 单工通信 v.s. 双工通信 1. 通信协议 通信协议就是计算机与计算机之间通过网络实现通信时事先达成的一种“约定”。这种“约定”使那些由不同厂商的设备、不同的CPU 以及不同的操作系统组成的计算…

记录一下MATLAB优化器出现的问题和解决

今天MATLAB优化器出了点问题。我想了想&#xff0c;决定解决一下&#xff0c;不然后面项目没有办法进行下去。 我忘了截图了。 具体来说&#xff0c;是出现了下面的问题。 Gurobi: Cplex: 在上次为了强化学习调整了Pytoch环境以后&#xff08;不知道是不是这个原因&#…

background 与 background-image

相同点&#xff1a;background 与 background-image都可以用于设置背景图 区别. background既可以用于设置背景图&#xff0c; 又可以用于设置CSS样式&#xff0c;还可以用于设置背景属性。 background-image只能用于设置背景图 background能设置的背景属性&#xff0c;如下&…

绝了!Stable Diffusion做AI治愈图片视频,用来做副业简直无敌!10分钟做一个爆款视频保姆教程

一 项目分析 这个治愈类视频的玩法是通过AI生成日常生活场景&#xff0c;制作的vlog&#xff0c;有这样的一个号&#xff0c;发布了几条作品&#xff0c;就涨粉了2000多&#xff0c;点赞7000多&#xff0c;非常的受欢迎。 下面给大家看下这种作品是什么样的&#xff0c;如图所…

大语言模型LLM基础:推理/不同模型/量化对显存、推理速度和性能的影响

通过本文&#xff0c;你将了解以下几个方面的内容&#xff1a; 要运行一个LLM需要多少显存&#xff1f;&#xff08;我的GPU可以运行多大LLM&#xff1f;&#xff09;不同LLM推理速度如何&#xff1f;量化对显存、推理速度和性能的影响&#xff1f;vLLM、DeepSeed等工具的加速…

智慧校园-档案管理系统总体概述

智慧校园档案管理系统&#xff0c;作为教育信息化进程中的重要一环&#xff0c;它运用现代信息技术的力量&#xff0c;彻底改变了传统档案管理的面貌&#xff0c;为学校档案资源的收集、整理、存储、检索与利用开辟了全新的途径。这一系统全面覆盖学生、教职工、教学科研及行政…

Rocky Linux设置静态IP

[connection] idens160 uuidcd246f67-c929-362a-809d-f1b44ddc5d25 typeethernet autoconnect-priority-999 interface-nameens160 timestamp1719094243[ethernet][ipv4] ## 在IPV4下面修改如下内容 methodmanual address192.…

常见的反爬手段和解决思路(爬虫与反爬虫)

常见的反爬手段和解决思路&#xff08;爬虫与反爬虫&#xff09; 学习目标1 服务器反爬的原因2 服务器长反什么样的爬虫&#xff08;1&#xff09;十分低级的应届毕业生&#xff08;2&#xff09;十分低级的创业小公司&#xff08;3&#xff09;不小心写错了没人去停止的失控小…

nuxt实现vuex持久化

前言&#xff1a; 此处不借助插件实现 store 本地持久化 所有状态持久化 使用 vuex 里面的 replaceState 方法还原 store 的根状态 API 参考 | Vuex 创建 store-cache.js 文件 在 plugins 目录下创建 store-cache.js 文件&#xff1b; store-cache.js export default (ctx) &g…

深度之眼(二十八)——神经网络基础知识(三)-卷积神经网络

文章目录 一、前言二、卷积操作2.1 填充&#xff08;padding&#xff09;2.2 步长2.3 输出特征图尺寸计算2.4 多通道卷积 三、池化操作四、Lenet-5及CNN结构进化史4.1 Lenet-5 一、前言 卷积神经网络–AlexNet(最牛)-2012 Lenet-5-大规模商用&#xff08;1989&#xff09; 二、…

如何保护磁盘数据?电脑磁盘数据怎么保护?

电脑磁盘是存储数据的基础&#xff0c;可以将各种重要数据保存在其中。为了避免数据泄露&#xff0c;我们需要保护磁盘数据。那么&#xff0c;电脑磁盘数据怎么保护呢&#xff1f;下面我们就一起来了解一下吧。 文件夹加密超级大师 文件夹加密超级大师是一款优秀的电脑数据加密…

收银系统源码-千呼新零售2.0【宠物、养生、大健康行业解决方案】

千呼新零售2.0系统是零售行业连锁店一体化收银系统&#xff0c;包括线下收银线上商城连锁店管理ERP管理商品管理供应商管理会员营销等功能为一体&#xff0c;线上线下数据全部打通。 适用于商超、便利店、水果、生鲜、母婴、服装、零食、百货、宠物、中医养生、大健康等连锁店…

ubuntu丢失网络/网卡的一种原因解决方案

现象 开机进入ubuntu后发现没有网络&#xff0c;无论是在桌面顶部状态栏的快捷键 还是 系统设置中&#xff0c;都没有”有线网“和”无线网“的选项&#xff0c;”代理“的选项是有的使用数据线连接电脑和手机&#xff0c;手机开启”通过usb共享网络“&#xff0c;还是没有任何…

【shell脚本实战案例】主机状态监控脚本

文章目录 案例需求脚本应用场景解决问题脚本思路实现代码 &#x1f308;你好呀&#xff01;我是 山顶风景独好 &#x1f388;欢迎踏入我的博客世界&#xff0c;能与您在此邂逅&#xff0c;真是缘分使然&#xff01;&#x1f60a; &#x1f338;愿您在此停留的每一刻&#xff0c…